93 Commits

Author SHA1 Message Date
c45a30675c feat: Finish updating resume template
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m12s
Build Docker / BuildImage (push) Successful in 1m24s
2025-11-04 17:24:28 +11:00
9e8e23165e feat: Start on new resume layout 2025-11-04 17:24:28 +11:00
53e05922bf fix: Add curl to dockerfiel for CD healthchecks
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m17s
Build Docker / BuildImage (push) Successful in 2m6s
2025-11-03 13:51:01 +11:00
0be0dad1b2 feat: Add new dockerfile to shrink image
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m57s
Build Docker / BuildImage (push) Successful in 3m42s
2025-11-03 13:44:19 +11:00
f404d55935 feat: Add phone number and city to resume
All checks were successful
Build Docker / BuildImage (push) Successful in 1m5s
Check Code Quality / RuffCheck (push) Successful in 1m8s
2025-11-01 18:03:03 +11:00
009c2b430c feat: Update projects
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m22s
Build Docker / BuildImage (push) Successful in 1m39s
2025-11-01 13:19:15 +11:00
ed96fbcc29 fix: Add curl to Dockerfile for coolify healthchecks
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m19s
Build Docker / BuildImage (push) Successful in 2m11s
2025-11-01 12:49:45 +11:00
4555ef5da2 Merge pull request 'Add new uv python3 manager' (#5) from feat/uv into main
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
Check Code Quality / RuffCheck (push) Successful in 1m7s
Reviewed-on: #5
2025-11-01 12:41:28 +11:00
1335a73eb6 fix: Remove requirement.txt to stop nixpacks using pip
All checks were successful
Build Docker / BuildImage (push) Successful in 1m6s
Check Code Quality / RuffCheck (push) Successful in 1m17s
2025-11-01 12:31:47 +11:00
b8f3039629 feat: Update Dockerfile to use new UV system
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m44s
Build Docker / BuildImage (push) Successful in 3m15s
Also fixes error in spotify refreshing
2025-11-01 12:17:25 +11:00
1888160fa5 fix: Update pyproject version to match latest git tag 2025-11-01 11:50:51 +11:00
7dd0f839cf feat: Add initial uv package info 2025-11-01 11:44:44 +11:00
5a0068586a feat: Cleanup resume summary
All checks were successful
Build Docker / BuildImage (push) Successful in 1m3s
Check Code Quality / RuffCheck (push) Successful in 1m8s
2025-10-31 15:16:48 +11:00
8079780c08 Merge pull request 'Refactor blueprints to make them easier to import' (#4) from feat/blueprint_imports into main
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m5s
Build Docker / BuildImage (push) Successful in 1m10s
Reviewed-on: #4
2025-10-30 21:44:46 +11:00
72b8dae35e fix: Update curl tools page to use new demo data
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
Check Code Quality / RuffCheck (push) Successful in 1m11s
2025-10-30 21:35:48 +11:00
323ace5775 feat: Remove old demo scripts
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 56s
Build Docker / BuildImage (push) Successful in 1m2s
2025-10-30 21:34:35 +11:00
c2803e372a feat: Dynamically load tool demos and start tracking BSdesign
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 58s
Build Docker / BuildImage (push) Successful in 1m0s
2025-10-30 21:29:27 +11:00
2a9e704f29 feat: Add zellij demo
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 59s
Build Docker / BuildImage (push) Successful in 1m4s
2025-10-30 20:52:20 +11:00
0c490625a9 fix: Add apt update before install python
All checks were successful
Build Docker / BuildImage (push) Successful in 47s
Check Code Quality / RuffCheck (push) Successful in 58s
2025-10-30 20:39:27 +11:00
b9753617ad fix: Remove sudo from action
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 17s
Build Docker / BuildImage (push) Successful in 1m2s
2025-10-30 20:38:09 +11:00
b87d19c5d9 fix: Manually install python3
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 13s
Build Docker / BuildImage (push) Successful in 47s
2025-10-30 20:36:29 +11:00
67e8b4cf7e fix: Try using a new container
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 31s
Build Docker / BuildImage (push) Successful in 58s
2025-10-30 20:33:02 +11:00
bfc6652f29 fix: Install python first
Some checks failed
Build Docker / BuildImage (push) Successful in 55s
Check Code Quality / RuffCheck (push) Failing after 1m35s
2025-10-30 20:28:31 +11:00
38372c0cff fix: Manually install ruff
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 17s
Build Docker / BuildImage (push) Successful in 58s
2025-10-30 20:27:00 +11:00
dd64313006 fix: Set action git url
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 22s
Build Docker / BuildImage (push) Successful in 47s
2025-10-30 20:19:56 +11:00
9e20a6171a feat: Add ruff check
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 25s
Build Docker / BuildImage (push) Successful in 57s
2025-10-30 20:15:10 +11:00
da347fd860 feat: Add max width for Now page contents
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
2025-10-30 20:06:32 +11:00
776b7de753 fix: Replace actions.json in the main server.py
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
2025-10-30 19:58:29 +11:00
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
4ae6f7bb99 feat: Optimise profile image
All checks were successful
Build Docker / BuildImage (push) Successful in 55s
2025-10-26 22:24:28 +11:00
d4d6b47225 feat: Add spotify to api routes
All checks were successful
Build Docker / BuildImage (push) Successful in 55s
2025-10-26 21:31:30 +11:00
9809fe0695 feat: Add spotify to curl route
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
2025-10-26 21:14:55 +11:00
3522389422 fix: Improve Spotify widget visibility
All checks were successful
Build Docker / BuildImage (push) Successful in 2m55s
2025-10-26 21:00:12 +11:00
2979d3c4de feat: Update python version in docker
All checks were successful
Build Docker / BuildImage (push) Successful in 2m38s
2025-10-26 20:47:01 +11:00
a8b2c02164 feat: Add initial spotify widget
All checks were successful
Build Docker / BuildImage (push) Successful in 57s
2025-10-26 20:43:48 +11:00
372ba908b8 feat: Add tools to navbar
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
2025-10-26 19:36:25 +11:00
1145b9205c Merge pull request 'Add initial ASCII art for curl connections' (#2) from feat/ascii_curl into main
All checks were successful
Build Docker / BuildImage (push) Successful in 50s
Reviewed-on: #2
2025-10-26 19:00:17 +11:00
a71c5b6663 feat: Update ascii templates to be nicer
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
2025-10-26 18:47:25 +11:00
724e800201 feat: Update curl template for index
All checks were successful
Build Docker / BuildImage (push) Successful in 53s
2025-10-26 18:43:25 +11:00
abcaa9283d feat: Add tools curl page 2025-10-26 18:43:25 +11:00
e175f68d25 feat: Add initial ascii art for curl connections 2025-10-26 18:43:25 +11:00
80b6a9bf46 feat: Update index page
All checks were successful
Build Docker / BuildImage (push) Successful in 57s
2025-10-26 18:42:22 +11:00
b089b8c0a8 feat: Add new tools api route
All checks were successful
Build Docker / BuildImage (push) Successful in 52s
2025-10-26 18:27:36 +11:00
8f774ba8f0 feat: Added tools page
All checks were successful
Build Docker / BuildImage (push) Successful in 2m9s
2025-10-26 18:00:18 +11:00
f4f5f47ee7 feat: Cleanup software blog style
All checks were successful
Build Docker / BuildImage (push) Successful in 2m41s
2025-10-24 16:10:28 +11:00
16f17a9486 feat: Add Software I use blog post
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
2025-10-23 15:00:05 +11:00
72483674f6 feat: Add now page for OCT
All checks were successful
Build Docker / BuildImage (push) Successful in 4m30s
2025-10-23 14:27:49 +11:00
b69c7f381b feat: Cleanup duplicate script code
All checks were successful
Build Docker / BuildImage (push) Successful in 59s
2025-10-16 17:37:48 +11:00
d7d4dbed8b feat: Add new status and ping route and update help menu
All checks were successful
Build Docker / BuildImage (push) Successful in 1m1s
2025-10-16 17:10:09 +11:00
2437b19836 feat: Add curl to container
All checks were successful
Build Docker / BuildImage (push) Successful in 4m33s
2025-10-16 16:57:41 +11:00
abd23e0eb8 fix: Add dateutil to requirements
All checks were successful
Build Docker / BuildImage (push) Successful in 2m59s
2025-10-16 16:54:16 +11:00
57a4b977ec feat: Add tool to estimate date of a webpage
All checks were successful
Build Docker / BuildImage (push) Successful in 2m34s
2025-10-16 16:48:26 +11:00
7f591e2724 fix: Cleanup blueprint names and add tests
All checks were successful
Build Docker / BuildImage (push) Successful in 2m17s
2025-10-13 15:32:31 +11:00
3d5c16f9cb fix: Verify legacy API redirects exist
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
This fixes an infinite redirect loop
2025-10-11 22:45:06 +11:00
fdb5f84c92 feat: Update config pulled from cloud 2025-10-11 22:32:29 +11:00
eaf363ee27 feat: Add curl support for blog pages
All checks were successful
Build Docker / BuildImage (push) Successful in 53s
2025-10-11 19:35:35 +11:00
0ea9db3473 feat: Update api routes to use similar json format to other routes 2025-10-11 19:16:50 +11:00
8d6acca5e9 feat: Add error message to header for HTML error responses 2025-10-11 18:55:24 +11:00
bfc1f0839a feat: Move getGitCommit from api to tools
All checks were successful
Build Docker / BuildImage (push) Successful in 1m4s
2025-10-11 18:51:22 +11:00
258061c64d feat: Use tools.json_response in hosting route
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
2025-10-11 18:14:20 +11:00
399ac5f0da feat: Move acme to blueprint and cleanup json responses
All checks were successful
Build Docker / BuildImage (push) Successful in 57s
2025-10-11 18:08:00 +11:00
74362de02a feat: Add better error messages to podcast routes
All checks were successful
Build Docker / BuildImage (push) Successful in 52s
2025-10-11 17:59:06 +11:00
9f7b93b8a1 feat: Move podcast routes to podcast blueprint
All checks were successful
Build Docker / BuildImage (push) Successful in 2m15s
2025-10-11 17:56:01 +11:00
665921d046 feat: Update about page info
All checks were successful
Build Docker / BuildImage (push) Successful in 58s
2025-10-11 17:47:03 +11:00
84cf772273 fix: Update index about section 2025-10-11 17:45:21 +11:00
22cd49a012 feat: Move error responses to new function in tools.py
All checks were successful
Build Docker / BuildImage (push) Successful in 1m6s
2025-10-11 17:39:46 +11:00
00d035a0e8 fix: Security issue in download route and cleanup
All checks were successful
Build Docker / BuildImage (push) Successful in 2m13s
2025-10-11 17:01:20 +11:00
fc56cafab8 feat: Split code into modules
All checks were successful
Build Docker / BuildImage (push) Successful in 4m7s
2025-10-10 22:58:06 +11:00
eee87e6ca7 fix: Cleanup code to follow linting 2025-10-10 21:35:47 +11:00
09852f19b6 feat: Move solana create transaction to new file
All checks were successful
Build Docker / BuildImage (push) Successful in 4m18s
2025-10-10 21:17:23 +11:00
8b464cd89d feat: Cleanup function names 2025-10-10 20:48:38 +11:00
98597768f3 feat: Update resume summary
All checks were successful
Build Docker / BuildImage (push) Successful in 2m31s
Make api endpoints more consistent
2025-10-09 16:04:07 +11:00
08f80ddb5c feat: Add some better checks
All checks were successful
Build Docker / BuildImage (push) Successful in 50s
2025-08-25 17:23:24 +10:00
33fd8136a7 feat: Add some more validation
All checks were successful
Build Docker / BuildImage (push) Successful in 1m2s
2025-08-24 21:26:14 +10:00
b2943bfeac feat: Add rate limits to exquiry
All checks were successful
Build Docker / BuildImage (push) Successful in 2m16s
2025-08-24 21:14:38 +10:00
c3c7c86a66 feat: Add now page for Aug 15
All checks were successful
Build Docker / BuildImage (push) Successful in 57s
2025-08-15 21:37:03 +10:00
8563a6857f feat: Update hosting to have presets
All checks were successful
Build Docker / BuildImage (push) Successful in 2m31s
2025-08-15 16:31:45 +10:00
1650d25d0f feat: Finish hosting enquiry page
All checks were successful
Build Docker / BuildImage (push) Successful in 2m12s
2025-08-14 16:01:46 +10:00
f4ee2297a7 feat: Add hosting page
All checks were successful
Build Docker / BuildImage (push) Successful in 6m14s
2025-08-13 12:46:43 +10:00
35ced02977 feat: Add new now page for July
All checks were successful
Build Docker / BuildImage (push) Successful in 4m4s
2025-07-21 11:15:36 +10:00
45709632d5 Merge branch 'feat/preloader'
All checks were successful
Build Docker / BuildImage (push) Successful in 1m13s
2025-07-03 14:19:46 +10:00
95 changed files with 4911 additions and 1710 deletions

33
.dockerignore Normal file
View File

@@ -0,0 +1,33 @@
# Bytecode and virtualenvs
__pycache__/
*.pyc
*.pyo
.venv/
.vscode/
.vs/
.ruff_check/
.env
# Pycache in subdirectories
**/__pycache__/
**/*.pyc
**/*.pyo
# Git and CI
.git/
.gitea/
testing/
tests/
# Build and docs
Dockerfile
NathanWoodburn.bsdesign
LICENSE.txt
README.md
# Development caches
*.tmp
*.log

View File

@@ -0,0 +1,18 @@
name: Check Code Quality
run-name: Ruff CI
on:
push:
jobs:
RuffCheck:
runs-on: [ubuntu-latest, amd]
steps:
- uses: actions/checkout@v2
- name: Set up Python
run: |
apt update
apt install -y python3 python3-pip
- name: Install Ruff
run: pip install ruff
- name: Run Ruff
run: ruff check .

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

View File

@@ -1,17 +1,62 @@
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
# syntax=docker/dockerfile:1
### Build stage ###
FROM python:3.13-alpine AS build
# Install build dependencies for Pillow and other native wheels
RUN apk add --no-cache \
build-base \
jpeg-dev zlib-dev freetype-dev
# Copy uv (fast Python package manager)
COPY --from=ghcr.io/astral-sh/uv:0.8.21 /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \
python3 -m pip install -r requirements.txt
# Install dependencies into a virtual environment
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked
COPY . /app
# Copy only app source files
COPY blueprints blueprints
COPY main.py server.py curl.py tools.py mail.py ./
COPY templates templates
COPY data data
COPY pwa pwa
COPY .well-known .well-known
# Add mount point for data volume
# VOLUME /data
# Clean up caches and pycache
RUN rm -rf /root/.cache/uv
RUN find . -type d -name "__pycache__" -exec rm -rf {} +
ENTRYPOINT ["python3"]
CMD ["main.py"]
FROM builder AS dev-envs
### Runtime stage ###
FROM python:3.13-alpine AS runtime
ENV PATH="/app/.venv/bin:$PATH"
# Create non-root user
RUN addgroup -g 1001 appgroup && \
adduser -D -u 1001 -G appgroup -h /app appuser
WORKDIR /app
RUN apk add --no-cache curl
# Copy only whats needed for runtime
COPY --from=build --chown=appuser:appgroup /app/.venv /app/.venv
COPY --from=build --chown=appuser:appgroup /app/blueprints /app/blueprints
COPY --from=build --chown=appuser:appgroup /app/templates /app/templates
COPY --from=build --chown=appuser:appgroup /app/data /app/data
COPY --from=build --chown=appuser:appgroup /app/pwa /app/pwa
COPY --from=build --chown=appuser:appgroup /app/.well-known /app/.well-known
COPY --from=build --chown=appuser:appgroup /app/main.py /app/
COPY --from=build --chown=appuser:appgroup /app/server.py /app/
COPY --from=build --chown=appuser:appgroup /app/curl.py /app/
COPY --from=build --chown=appuser:appgroup /app/tools.py /app/
COPY --from=build --chown=appuser:appgroup /app/mail.py /app/
USER appuser
EXPOSE 5000
ENTRYPOINT ["python3", "main.py"]

BIN
NathanWoodburn.bsdesign Normal file

Binary file not shown.

36
blueprints/acme.py Normal file
View File

@@ -0,0 +1,36 @@
from flask import Blueprint, request
import os
from cloudflare import Cloudflare
from tools import json_response
app = Blueprint('acme', __name__)
@app.route("/hnsdoh-acme", methods=["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)
if "txt" not in request.json or "auth" not in request.json:
return json_response(request, "400 Bad Request", 400)
txt = request.json["txt"]
auth = request.json["auth"]
if auth != os.getenv("CF_AUTH"):
return json_response(request, "401 Unauthorized", 401)
cf = Cloudflare(api_token=os.getenv("CF_TOKEN"))
zone = cf.zones.list(name="hnsdoh.com").to_dict()
zone_id = zone["result"][0]["id"] # type: ignore
existing_records = cf.dns.records.list(
zone_id=zone_id, type="TXT", name="_acme-challenge.hnsdoh.com" # type: ignore
).to_dict()
record_id = existing_records["result"][0]["id"] # type: ignore
cf.dns.records.delete(dns_record_id=record_id, zone_id=zone_id)
cf.dns.records.create(
zone_id=zone_id,
type="TXT",
name="_acme-challenge",
content=txt,
)
return json_response(request, "Success", 200)

324
blueprints/api.py Normal file
View File

@@ -0,0 +1,324 @@
from flask import Blueprint, request, jsonify
import os
import datetime
import requests
import re
from mail import sendEmail
from tools import getClientIP, getGitCommit, json_response, parse_date, get_tools_data
from blueprints import sol
from dateutil import parser as date_parser
from blueprints.spotify import get_spotify_track
# Constants
HTTP_OK = 200
HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
HTTP_NOT_FOUND = 404
HTTP_UNSUPPORTED_MEDIA = 415
HTTP_SERVER_ERROR = 500
app = Blueprint('api', __name__, url_prefix='/api/v1')
# Register solana blueprint
app.register_blueprint(sol.app)
# Load configuration
NC_CONFIG = requests.get(
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
).json()
if 'time-zone' not in NC_CONFIG:
NC_CONFIG['time-zone'] = 10
@app.route("/", strict_slashes=False)
@app.route("/help")
def help():
"""Provide API documentation and help."""
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",
"/project": "Get the current project from git",
"/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)",
"/tools": "Get a list of tools used by Nathan Woodburn",
"/playing": "Get the currently playing Spotify track",
"/status": "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"
},
"base_url": "/api/v1",
"version": getGitCommit(),
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/status")
@app.route("/ping")
def status():
return json_response(request, "200 OK", HTTP_OK)
@app.route("/version")
def version():
"""Get the current version of the website."""
return jsonify({"version": getGitCommit()})
@app.route("/time")
def time():
"""Get the current time in the configured timezone."""
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
timezone = datetime.timezone(offset=timezone_offset)
current_time = datetime.datetime.now(tz=timezone)
return jsonify({
"timestring": current_time.strftime("%A, %B %d, %Y %I:%M %p"),
"timestamp": current_time.timestamp(),
"timezone": NC_CONFIG["time-zone"],
"timeISO": current_time.isoformat(),
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/timezone")
def timezone():
"""Get the current timezone setting."""
return jsonify({
"timezone": NC_CONFIG["time-zone"],
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/message")
def message():
"""Get the message from the configuration."""
return jsonify({
"message": NC_CONFIG["message"],
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/ip")
def ip():
"""Get the client's IP address."""
return jsonify({
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/email", methods=["POST"])
def email_post():
"""Send an email via the API (requires API key)."""
# Verify json
if not request.is_json:
return json_response(request, "415 Unsupported Media Type", HTTP_UNSUPPORTED_MEDIA)
# Check if api key sent
data = request.json
if not data:
return json_response(request, "400 Bad Request", HTTP_BAD_REQUEST)
if "key" not in data:
return json_response(request, "400 Bad Request 'key' missing", HTTP_BAD_REQUEST)
if data["key"] != os.getenv("EMAIL_KEY"):
return json_response(request, "401 Unauthorized", HTTP_UNAUTHORIZED)
# TODO: Add client info to email
return sendEmail(data)
@app.route("/project")
def project():
"""Get information about the current git project."""
gitinfo = {
"website": None,
}
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"]
gitinfo["name"] = repo_name
gitinfo["description"] = repo_description
gitinfo["url"] = git["repo"]["html_url"]
if "website" in git["repo"]:
gitinfo["website"] = git["repo"]["website"]
except Exception as e:
print(f"Error getting git data: {e}")
return json_response(request, "500 Internal Server Error", HTTP_SERVER_ERROR)
return jsonify({
"repo_name": repo_name,
"repo_description": repo_description,
"repo": gitinfo,
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/tools")
def tools():
"""Get a list of tools used by Nathan Woodburn."""
try:
tools = get_tools_data()
except Exception as e:
print(f"Error getting tools data: {e}")
return json_response(request, "500 Internal Server Error", HTTP_SERVER_ERROR)
return json_response(request, {"tools": tools}, HTTP_OK)
@app.route("/playing")
def playing():
"""Get the currently playing Spotify track."""
track_info = get_spotify_track()
if "error" in track_info:
return json_response(request, track_info, HTTP_OK)
return json_response(request, {"spotify": track_info}, HTTP_OK)
@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():
url = request.args.get("url")
if not url:
return json_response(request, "400 Bad Request 'url' missing", HTTP_BAD_REQUEST)
verbose = request.args.get("verbose", "").lower() in ["true", "1", "yes", "y"]
if not url.startswith(("https://", "http://")):
return json_response(request, "400 Bad Request 'url' invalid", HTTP_BAD_REQUEST)
try:
r = requests.get(url, timeout=5)
r.raise_for_status()
except requests.exceptions.RequestException as e:
return json_response(request, f"400 Bad Request 'url' unreachable: {e}", HTTP_BAD_REQUEST)
page_text = r.text
# Remove ordinal suffixes globally
page_text = re.sub(r'(\d+)(st|nd|rd|th)', r'\1', page_text, flags=re.IGNORECASE)
# Remove HTML comments
page_text = re.sub(r'<!--.*?-->', '', page_text, flags=re.DOTALL)
date_patterns = [
r'(\d{4})[/-](\d{1,2})[/-](\d{1,2})', # YYYY-MM-DD
r'(\d{1,2})[/-](\d{1,2})[/-](\d{4})', # DD-MM-YYYY
r'(?:Last updated:|Updated:|Updated last:)?\s*(\d{1,2})\s+([A-Za-z]{3,9})[, ]?\s*(\d{4})', # DD Month YYYY
r'(?:\b\w+\b\s+){0,3}([A-Za-z]{3,9})\s+(\d{1,2}),?\s*(\d{4})', # Month DD, YYYY with optional words
r'\b(\d{4})(\d{2})(\d{2})\b', # YYYYMMDD
r'(?:Last updated:|Updated:|Last update)?\s*([A-Za-z]{3,9})\s+(\d{4})', # Month YYYY only
]
# Structured data patterns
json_date_patterns = {
r'"datePublished"\s*:\s*"([^"]+)"': "published",
r'"dateModified"\s*:\s*"([^"]+)"': "modified",
r'<meta\s+(?:[^>]*?)property\s*=\s*"article:published_time"\s+content\s*=\s*"([^"]+)"': "published",
r'<meta\s+(?:[^>]*?)property\s*=\s*"article:modified_time"\s+content\s*=\s*"([^"]+)"': "modified",
r'<time\s+datetime\s*=\s*"([^"]+)"': "published"
}
found_dates = []
# Extract content dates
for idx, pattern in enumerate(date_patterns):
for match in re.findall(pattern, page_text):
if not match:
continue
groups = match[-3:] # last three elements
found_dates.append([groups, idx, "content"])
# Extract structured data dates
for pattern, date_type in json_date_patterns.items():
for match in re.findall(pattern, page_text):
try:
dt = date_parser.isoparse(match)
formatted_date = dt.strftime('%Y-%m-%d')
found_dates.append([[formatted_date], -1, date_type])
except (ValueError, TypeError):
continue
if not found_dates:
return json_response(request, "Date not found on page", HTTP_BAD_REQUEST)
today = datetime.date.today()
tolerance_date = today + datetime.timedelta(days=1) # Allow for slight future dates (e.g., time zones)
# When processing dates
processed_dates = []
for date_groups, pattern_format, date_type in found_dates:
if pattern_format == -1:
# Already formatted date
try:
dt = datetime.datetime.strptime(date_groups[0], "%Y-%m-%d").date()
except ValueError:
continue
else:
parsed_date = parse_date(date_groups)
if not parsed_date:
continue
dt = datetime.datetime.strptime(parsed_date, "%Y-%m-%d").date()
# Only keep dates in the past (with tolerance)
if dt <= tolerance_date:
date_obj = {"date": dt.strftime("%Y-%m-%d"), "type": date_type}
if verbose:
if pattern_format == -1:
date_obj.update({"source": "metadata", "pattern_used": pattern_format, "raw": date_groups[0]})
else:
date_obj.update({"source": "content", "pattern_used": pattern_format, "raw": " ".join(date_groups)})
processed_dates.append(date_obj)
if not processed_dates:
if verbose:
return jsonify({
"message": "No valid dates found on page",
"found_dates": found_dates,
"processed_dates": processed_dates
}), HTTP_BAD_REQUEST
return json_response(request, "No valid dates found on page", HTTP_BAD_REQUEST)
# Sort dates and return latest
processed_dates.sort(key=lambda x: x["date"])
latest = processed_dates[-1]
response = {"latest": latest["date"], "type": latest["type"]}
if verbose:
response["dates"] = processed_dates
return json_response(request, response, HTTP_OK)

View File

@@ -1,22 +1,27 @@
import os
from flask import render_template
from datetime import datetime
from flask import Blueprint, render_template, request, jsonify
import markdown
from markdown.extensions.codehilite import CodeHiliteExtension
from markdown.extensions.fenced_code import FencedCodeExtension
from bs4 import BeautifulSoup
import re
from tools import isCLI, getClientIP, getHandshakeScript
app = Blueprint('blog', __name__, url_prefix='/blog')
def list_blog_page_files():
def list_page_files():
blog_pages = os.listdir("data/blog")
# Sort pages by modified time, newest first
blog_pages.sort(
key=lambda x: os.path.getmtime(os.path.join("data/blog", x)), reverse=True)
# 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_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
@@ -26,12 +31,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(
@@ -48,7 +53,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()
content = p.decode_contents() # type: ignore
# Check for likely numbered step structure
if re.search(r'1\.\s', content):
@@ -65,7 +70,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>"
@@ -81,14 +87,14 @@ def fix_numbered_lists(html):
return str(soup)
def render_blog_home(handshake_scripts=None):
def render_home(handshake_scripts: str | None = 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">
<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
]
@@ -100,3 +106,59 @@ def render_blog_home(handshake_scripts=None):
blogs=blog_pages,
handshake_scripts=handshake_scripts,
)
@app.route("/", strict_slashes=False)
def index():
if not isCLI(request):
return render_home(handshake_scripts=getHandshakeScript(request.host))
# Get a list of pages
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
]
# Render the template
return jsonify({
"status": 200,
"message": "Check out my various blog postsa",
"ip": getClientIP(request),
"blogs": blog_pages
}), 200
@app.route("/<path:path>")
def path(path):
if not isCLI(request):
return render_page(path, handshake_scripts=getHandshakeScript(request.host))
# Convert md to html
if not os.path.exists(f"data/blog/{path}.md"):
return render_template("404.html"), 404
with open(f"data/blog/{path}.md", "r") as f:
content = f.read()
# Get the title from the file name
title = path.replace("_", " ")
return jsonify({
"status": 200,
"message": f"Blog post: {title}",
"ip": getClientIP(request),
"title": title,
"content": content,
"download": f"/blog/{path}.md"
}), 200
@app.route("/<path:path>.md")
def path_md(path):
if not os.path.exists(f"data/blog/{path}.md"):
return render_template("404.html"), 404
with open(f"data/blog/{path}.md", "r") as f:
content = f.read()
# Return the raw markdown file
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}

201
blueprints/now.py Normal file
View File

@@ -0,0 +1,201 @@
from flask import Blueprint, render_template, make_response, request, jsonify
import datetime
import os
from tools import getHandshakeScript, error_response, isCLI
from curl import get_header, MAX_WIDTH
from bs4 import BeautifulSoup
import re
# Create blueprint
app = Blueprint('now', __name__, url_prefix='/now')
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"
]
now_pages.sort(reverse=True)
return now_pages
def list_dates():
now_pages = list_page_files()
now_dates = [page.split(".")[0] for page in now_pages]
return now_dates
def get_latest_date(formatted=False):
if formatted:
date = list_dates()[0]
date = datetime.datetime.strptime(date, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y")
return date
return list_dates()[0]
def render_latest(handshake_scripts=None):
now_page = list_dates()[0]
return render(now_page, handshake_scripts=handshake_scripts)
def render(date, handshake_scripts=None):
# If the date is not available, render the latest page
if date is None:
return render_latest(handshake_scripts=handshake_scripts)
# Remove .html
date = date.removesuffix(".html")
if date not in list_dates():
return error_response(request)
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)
def render_curl(date=None):
# If the date is not available, render the latest page
if date is None:
date = get_latest_date()
# 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
# Set max width for text wrapping
# Wrap text manually
wrapped_lines = []
for line in text.splitlines():
while len(line) > MAX_WIDTH:
# Find last space within max_width
split_at = line.rfind(' ', 0, MAX_WIDTH)
if split_at == -1:
split_at = MAX_WIDTH
wrapped_lines.append(line[:split_at].rstrip())
line = line[split_at:].lstrip()
wrapped_lines.append(line)
text = "\n".join(wrapped_lines)
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():
if isCLI(request):
return render_curl()
return render_latest(handshake_scripts=getHandshakeScript(request.host))
@app.route("/<path:path>")
def path(path):
if isCLI(request):
return render_curl(path)
return render(path, handshake_scripts=getHandshakeScript(request.host))
@app.route("/old", strict_slashes=False)
def old():
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 += 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
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=getHandshakeScript(request.host), now_pages=html
)
@app.route("/now.rss")
@app.route("/now.xml")
@app.route("/rss.xml")
def rss():
host = "https://" + request.host
if ":" in request.host:
host = "http://" + request.host
# Generate RSS feed
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")
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 json():
now_pages = list_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)

59
blueprints/podcast.py Normal file
View File

@@ -0,0 +1,59 @@
from flask import Blueprint, make_response, request
from tools import error_response
import requests
app = Blueprint('podcast', __name__)
@app.route("/ID1")
def index():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1")
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"]}
)
@app.route("/ID1/")
def contents():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1/")
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"]}
)
@app.route("/ID1/<path:path>")
def path(path):
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1/" + path)
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"]}
)
@app.route("/ID1.xml")
def xml():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1.xml")
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"]}
)
@app.route("/podsync.opml")
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
app = 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)
@app.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
@app.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
@app.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

130
blueprints/spotify.py Normal file
View File

@@ -0,0 +1,130 @@
from flask import redirect, request, Blueprint, url_for
from tools import json_response
import os
import requests
import time
import base64
app = Blueprint('spotify', __name__, url_prefix='/spotify')
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
ALLOWED_SPOTIFY_USER_ID = os.getenv("SPOTIFY_USER_ID")
SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
SPOTIFY_CURRENTLY_PLAYING_URL = "https://api.spotify.com/v1/me/player/currently-playing"
SCOPE = "user-read-currently-playing user-read-playback-state"
ACCESS_TOKEN = None
REFRESH_TOKEN = os.getenv("SPOTIFY_REFRESH_TOKEN")
TOKEN_EXPIRES = 0
def refresh_access_token():
"""Refresh Spotify access token when expired."""
global ACCESS_TOKEN, TOKEN_EXPIRES
# If no refresh token, cannot proceed
if not REFRESH_TOKEN:
return None
# If still valid, reuse it
if ACCESS_TOKEN and time.time() < TOKEN_EXPIRES - 60:
return ACCESS_TOKEN
auth_str = f"{CLIENT_ID}:{CLIENT_SECRET}"
b64_auth = base64.b64encode(auth_str.encode()).decode()
data = {
"grant_type": "refresh_token",
"refresh_token": REFRESH_TOKEN,
}
headers = {"Authorization": f"Basic {b64_auth}"}
response = requests.post(SPOTIFY_TOKEN_URL, data=data, headers=headers)
if response.status_code != 200:
print("Failed to refresh token:", response.text)
return None
token_info = response.json()
ACCESS_TOKEN = token_info["access_token"]
TOKEN_EXPIRES = time.time() + token_info.get("expires_in", 3600)
return ACCESS_TOKEN
@app.route("/login")
def login():
auth_query = (
f"{SPOTIFY_AUTH_URL}?response_type=code&client_id={CLIENT_ID}"
f"&redirect_uri={url_for('spotify.callback', _external=True)}&scope={SCOPE}"
)
return redirect(auth_query)
@app.route("/callback")
def callback():
code = request.args.get("code")
if not code:
return "Authorization failed.", 400
data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": url_for("spotify.callback", _external=True),
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
}
response = requests.post(SPOTIFY_TOKEN_URL, data=data)
token_info = response.json()
if "access_token" not in token_info:
return json_response(request, {"error": "Failed to obtain token", "details": token_info}, 400)
access_token = token_info["access_token"]
me = requests.get(
"https://api.spotify.com/v1/me",
headers={"Authorization": f"Bearer {access_token}"}
).json()
if me.get("id") != ALLOWED_SPOTIFY_USER_ID:
return json_response(request, {"error": "Unauthorized user"}, 403)
global REFRESH_TOKEN
REFRESH_TOKEN = token_info.get("refresh_token")
print("Spotify authorization successful.")
print("Refresh Token:", REFRESH_TOKEN)
return redirect(url_for("spotify.currently_playing"))
@app.route("/", strict_slashes=False)
@app.route("/playing")
def currently_playing():
"""Public endpoint showing your current track."""
track = get_spotify_track()
return json_response(request, {"spotify":track}, 200)
def get_spotify_track():
"""Internal function to get current playing track without HTTP context."""
token = refresh_access_token()
if not token:
return {"error": "Failed to refresh access token"}
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(SPOTIFY_CURRENTLY_PLAYING_URL, headers=headers)
if response.status_code == 204:
return {"error": "Nothing is currently playing."}
elif response.status_code != 200:
return {"error": "Spotify API error", "status": response.status_code}
data = response.json()
if not data.get("item"):
return {"error": "Nothing is currently playing."}
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"],
"progress_ms": data.get("progress_ms",0),
"duration_ms": data["item"].get("duration_ms",1)
}
return track

9
blueprints/template.py Normal file
View File

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

63
blueprints/wellknown.py Normal file
View File

@@ -0,0 +1,63 @@
from flask import Blueprint, make_response, request, jsonify, send_from_directory, redirect
from tools import error_response
import os
app = Blueprint('well-known', __name__, url_prefix='/.well-known')
@app.route("/<path:path>")
def index(path):
return send_from_directory(".well-known", path)
@app.route("/wallets/<path:path>")
def wallets(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 error_response(request)
@app.route("/nostr.json")
def nostr():
# Get name parameter
name = request.args.get("name")
if name:
return jsonify(
{
"names": {
name: "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82"
}
}
)
return jsonify(
{
"names": {
"nathan": "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82",
"_": "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82",
}
}
)
@app.route("/xrp-ledger.toml")
def xrp():
# 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

132
curl.py Normal file
View File

@@ -0,0 +1,132 @@
from flask import render_template
from tools import getAddress, get_tools_data, getClientIP
import os
from functools import lru_cache
import requests
from blueprints.spotify import get_spotify_track
MAX_WIDTH = 80
def clean_path(path:str):
path = path.strip("/ ").lower()
# Strip any .html extension
if path.endswith(".html"):
path = path[:-5]
# If the path is empty, set it to "index"
if path == "":
path = "index"
return path
@lru_cache(maxsize=1)
def get_header():
with open("templates/header.ascii", "r") as f:
return f.read()
@lru_cache(maxsize=1)
def get_current_project():
git = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
headers={"Authorization": os.getenv("GIT_AUTH") if os.getenv("GIT_AUTH") else 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"]
if not repo_description:
return f"{repo_name}"
return f"{repo_name} - {repo_description}"
@lru_cache(maxsize=1)
def get_projects():
projectsreq = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos"
)
projects = projectsreq.json()
# Check for next page
pageNum = 1
while 'rel="next"' in projectsreq.headers["link"]:
projectsreq = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page="
+ str(pageNum)
)
projects += projectsreq.json()
pageNum += 1
# Sort by last updated
projectsList = sorted(
projects, key=lambda x: x["updated_at"], reverse=True)
projects = ""
projectNum = 0
includedNames = []
while len(includedNames) < 5 and projectNum < len(projectsList):
# Avoid duplicates
if projectsList[projectNum]["name"] in includedNames:
projectNum += 1
continue
includedNames.append(projectsList[projectNum]["name"])
project = projectsList[projectNum]
projects += f"""{project['name']} - {project['description'] if project['description'] else 'No description'}
{project['html_url']}
"""
projectNum += 1
return projects
def curl_response(request):
# Check if <path>.ascii exists
path = clean_path(request.path)
# Handle special cases
if path == "index":
# Get current project
return render_template("index.ascii",repo=get_current_project(), ip=getClientIP(request), spotify=get_spotify_track()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
if path == "projects":
# Get projects
return render_template("projects.ascii",header=get_header(),projects=get_projects()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
if path == "donate":
# Get donation info
return render_template("donate.ascii",header=get_header(),
HNS=getAddress("HNS"), BTC=getAddress("BTC"),
SOL=getAddress("SOL"), ETH=getAddress("ETH")
), 200, {'Content-Type': 'text/plain; charset=utf-8'}
if path == "donate/more":
coinList = os.listdir(".well-known/wallets")
coinList = [file for file in coinList if file[0] != "."]
coinList.sort()
return render_template("donate_more.ascii",header=get_header(),
coins=coinList
), 200, {'Content-Type': 'text/plain; charset=utf-8'}
# For other donation pages, fall back to ascii if it exists
if path.startswith("donate/"):
coin = path.split("/")[1]
address = getAddress(coin)
if address != "":
return render_template("donate_coin.ascii",header=get_header(),coin=coin.upper(),address=address), 200, {'Content-Type': 'text/plain; charset=utf-8'}
if path == "tools":
tools = get_tools_data()
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"):
return render_template(f"{path}.ascii",header=get_header()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
# Fallback to html if it exists
if os.path.exists(f"templates/{path}.html"):
return render_template(f"{path}.html")
# 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

@@ -0,0 +1,53 @@
G'day,
Just thought it might be useful to write down some of the software I use regularly. I've no clue if you'll find any useful :)
For a more complete list, check out [/tools](/tools)
<br>
## Overview
OS: Arch Linux | Because it is quick to update and has all the latest tools I can play with
DE: Hyprland | Feel free to check out my dotfiles if you're interested
Shell: ZSH
<br>
## Desktop Applications
[Obsidian](https://obsidian.md/) | Note taking app that stores everything in Markdown files
[Alacritty](https://alacritty.org/) | Terminal emulator
[Brave](https://brave.com/) | Browser with ad blocker built in
[VSCode](https://code.visualstudio.com/) | Yeah its heavy but I'm used to it
<br>
## Terminal Tools
[Zellij](https://zellij.dev/) | Easy to use terminal multiplexer
[Fx](https://fx.wtf/) | JSON parser with pretty colours. Similar to jq
[Zoxide](https://github.com/ajeetdsouza/zoxide) | cd but with fuzzy matching and other cool features
[Atuin](https://atuin.sh/) | Terminal history with fuzzy search
[Tmate](https://tmate.io/) | Terminal sharing. Useful when troubleshooting isses for remote users
[Eza](https://eza.rocks/) | Like ls but pretty
[Tre](https://github.com/dduan/tre) | Like tree but pretty
[Bat](https://github.com/sharkdp/bat) | Like cat but pretty. Syntax highlighting, line numbers, search, git integration and more
[Oh My ZSH](https://ohmyz.sh/) | Shell customization and plugins
<br>
## Server Management
[Proxmox](https://proxmox.com/en/) | Virtualization manager for my baremetal server
[Portainer](https://www.portainer.io/) | Docker container manager
[Coolify](https://coolify.io/) | Open source alternative to heroku. I use it to host a lot of different services
[Opnsense](https://opnsense.org/) | Firewall and router
[Nginx Proxy Manager](https://nginxproxymanager.com/) | Reverse proxy manager with a nice UI
[Tailscale](https://tailscale.com/) | VPN to let me access my network from anywhere
<br>
## Self-Hosting Services
[Authentik](https://goauthentik.io/) | Identity provider for single sign on
[Gitea](https://gitea.io/) | Git hosting service
[Nextcloud](https://nextcloud.com/) | Think Dropbox but self hosted
[Umami](https://umami.is/) | Self hosted web analytics
[Uptime Kuma](https://uptime.kuma.pet/) | Self hosted status page and monitoring tool
[PhotoPrism](https://photoprism.app/) | Self hosted photo management tool
[FreeScout](https://freescout.net/) | Self hosted email dashboard
[Transfer.sh](https://upload.woodburn.au/) | Self hosted file sharing service

Binary file not shown.

BIN
data/resume_support.pdf Normal file

Binary file not shown.

View File

@@ -10,7 +10,8 @@
"url": "https://domains.hns.au",
"img": "/assets/img/external/HNSAU.webp",
"name": "HNSAU Registry",
"description": "An easy to use DNS provider and domain reselling platform"
"description": "An easy to use DNS provider and domain reselling platform",
"enabled": false
},
{
"url": "https://hns.au",
@@ -22,7 +23,8 @@
"url": "https://hnshosting.au",
"img": "/favicon.png",
"name": "HNS Hosting",
"description": "Simple Wordpress hosting for Handshake domains with builtin SSL using DANE"
"description": "Simple Wordpress hosting for Handshake domains with builtin SSL using DANE",
"enabled": false
},
{
"url": "https://firewallet.au",
@@ -34,7 +36,8 @@
"url": "https://shakecities.com",
"img": "/assets/img/external/HNSW.png",
"name": "ShakeCities",
"description": "A single page website creator where each user's page on their free HNS domain"
"description": "A single page website creator where each user's page on their free HNS domain",
"enabled": false
},
{
"url": "https://git.woodburn.au",
@@ -66,5 +69,17 @@
"img": "https://ipfs.hnsproxy.au/fireportal.png",
"name": "FirePortal",
"description": "A Handshake domain IPFS gateway that allows you to access IPFS content using Handshake domains"
},
{
"url": "https://hsd.hns.au/",
"img": "/favicon.png",
"name": "Fire HSD",
"description": "A free public API for Handshake (HSD)"
},
{
"url": "https://time.c.woodburn.au/",
"img": "/favicon.png",
"name": "Timezone Converter",
"description": "A simple site and API for converting timezones"
}
]

171
data/tools.json Normal file
View File

@@ -0,0 +1,171 @@
[
{
"name":"Obsidian",
"type":"Desktop Applications",
"url":"https://obsidian.md/",
"description":"Note taking app that stores everything in Markdown files"
},
{
"name": "Alacritty",
"type": "Desktop Applications",
"url": "https://alacritty.org/",
"description": "A cross-platform, GPU-accelerated terminal emulator"
},
{
"name": "Brave",
"type": "Desktop Applications",
"url": "https://brave.com/",
"description": "Privacy-focused web browser"
},
{
"name": "VSCode",
"type": "Desktop Applications",
"url": "https://code.visualstudio.com/",
"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",
"type": "Terminal Tools",
"url": "https://zellij.dev/",
"description": "A terminal workspace and multiplexer",
"demo": "https://asciinema.c.woodburn.au/a/10"
},
{
"name": "Fx",
"type": "Terminal Tools",
"url": "https://fx.wtf/",
"description": "A command-line JSON viewer and processor",
"demo": "https://asciinema.c.woodburn.au/a/4"
},
{
"name": "Zoxide",
"type": "Terminal Tools",
"url": "https://github.com/ajeetdsouza/zoxide",
"description": "cd but with fuzzy matching and other cool features",
"demo": "https://asciinema.c.woodburn.au/a/5"
},
{
"name": "Atuin",
"type": "Terminal Tools",
"url": "https://atuin.sh/",
"description": "A next-generation shell history manager",
"demo": "https://asciinema.c.woodburn.au/a/6"
},
{
"name": "Tmate",
"type": "Terminal Tools",
"url": "https://tmate.io/",
"description": "Instant terminal sharing",
"demo": "https://asciinema.c.woodburn.au/a/7"
},
{
"name": "Eza",
"type": "Terminal Tools",
"url": "https://eza.rocks/",
"description": "A modern replacement for 'ls'",
"demo": "https://asciinema.c.woodburn.au/a/8"
},
{
"name": "Bat",
"type": "Terminal Tools",
"url": "https://github.com/sharkdp/bat",
"description": "A cat clone with syntax highlighting and Git integration",
"demo": "https://asciinema.c.woodburn.au/a/9"
},
{
"name": "Oh My Zsh",
"type": "Terminal Tools",
"url": "https://ohmyz.sh/",
"description": "A delightful community-driven framework for managing your Zsh configuration"
},
{
"name": "Proxmox",
"type": "Server Management",
"url": "https://www.proxmox.com/en",
"description": "Open-source server virtualization management solution"
},
{
"name": "Portainer",
"type": "Server Management",
"url": "https://www.portainer.io/",
"description": "Lightweight management UI which allows you to easily manage your Docker containers"
},
{
"name": "Coolify",
"type": "Server Management",
"url": "https://coolify.io/",
"description": "An open-source self-hosted Heroku alternative"
},
{
"name": "OpnSense",
"type": "Server Management",
"url": "https://opnsense.org/",
"description": "Open source, easy-to-use and easy-to-build FreeBSD based firewall and routing platform"
},
{
"name": "Nginx Proxy Manager",
"type": "Server Management",
"url": "https://nginxproxymanager.com/",
"description": "A powerful yet easy to use web interface for managing Nginx proxy hosts"
},
{
"name": "Tailscale",
"type": "Server Management",
"url": "https://tailscale.com/",
"description": "A zero-config VPN that just works"
},
{
"name": "Authentik",
"type": "Self-Hosting Services",
"url": "https://goauthentik.io/",
"description": "An open-source identity provider focused on flexibility and ease of use"
},
{
"name": "Uptime Kuma",
"type": "Self-Hosting Services",
"url": "https://uptime.kuma.pet/",
"description": "A fancy self-hosted monitoring tool"
},
{
"name": "Gitea",
"type": "Self-Hosting Services",
"url": "https://about.gitea.com/",
"description": "A painless self-hosted Git service"
},
{
"name": "Nextcloud",
"type": "Self-Hosting Services",
"url": "https://nextcloud.com/",
"description": "A suite of client-server software for creating and using file hosting services"
},
{
"name": "Umami",
"type": "Self-Hosting Services",
"url": "https://umami.is/",
"description": "A simple, fast, privacy-focused alternative to Google Analytics"
},
{
"name": "PhotoPrism",
"type": "Self-Hosting Services",
"url": "https://photoprism.app/",
"description": "AI-powered app for browsing, organizing & sharing your photo collection"
},
{
"name": "FreeScout",
"type": "Self-Hosting Services",
"url": "https://freescout.net/",
"description": "Self hosted email dashboard"
},
{
"name": "Vaultwarden",
"type": "Miscellaneous",
"url": "https://github.com/dani-garcia/vaultwarden",
"description": "Password manager server implementation compatible with Bitwarden clients"
}
]

19
mail.py
View File

@@ -22,7 +22,11 @@ import os
# }'
def validateSender(email):
domains = os.getenv("EMAIL_DOMAINS").split(",")
domains = os.getenv("EMAIL_DOMAINS")
if not domains:
return False
domains = domains.split(",")
for domain in domains:
if re.match(r".+@" + domain, email):
return True
@@ -84,8 +88,17 @@ def sendEmail(data):
# Sending the email
try:
with smtplib.SMTP_SSL(os.getenv("EMAIL_SMTP"), 465) as server:
server.login(os.getenv("EMAIL_USER"), os.getenv("EMAIL_PASS"))
host = os.getenv("EMAIL_SMTP")
user = os.getenv("EMAIL_USER")
password = os.getenv("EMAIL_PASS")
if host is None or user is None or password is None:
return jsonify({
"status": 500,
"error": "Email server not configured"
})
with smtplib.SMTP_SSL(host, 465) as server:
server.login(user, password)
server.sendmail(fromEmail, to, msg.as_string())
print("Email sent successfully.")
return jsonify({

10
main.py
View File

@@ -1,12 +1,6 @@
import time
from flask import Flask
from server import app
import server
from gunicorn.app.base import BaseApplication
import os
import dotenv
import sys
import json
class GunicornApp(BaseApplication):
@@ -17,8 +11,8 @@ class GunicornApp(BaseApplication):
def load_config(self):
for key, value in self.options.items():
if key in self.cfg.settings and value is not None:
self.cfg.set(key.lower(), value)
if key in self.cfg.settings and value is not None: # type: ignore
self.cfg.set(key.lower(), value) # type: ignore
def load(self):
return self.application

48
now.py
View File

@@ -1,48 +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]
#region Rendering
def render_now_page(date,handshake_scripts=None):
# If the date is not available, render the latest page
if date is None:
return render_latest_now(handshake_scripts=handshake_scripts)
# Remove .html
date = date.removesuffix(".html")
if not date in list_now_dates():
return render_template("404.html"), 404
date_formatted = datetime.strptime(date, "%y_%m_%d")
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
return render_template(f"now/{date}.html",DATE=date_formatted,handshake_scripts=handshake_scripts)
def render_latest_now(handshake_scripts=None):
now_page = list_now_dates()[0]
return render_now_page(now_page,handshake_scripts=handshake_scripts)
#endregion

25
pyproject.toml Normal file
View File

@@ -0,0 +1,25 @@
[project]
name = "nathanwoodburn-github-io"
version = "1.1.0"
description = "Nathan.Woodburn Personal Website"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"ansi2html>=1.9.2",
"beautifulsoup4>=4.14.2",
"cachetools>=6.2.1",
"cloudflare>=4.3.1",
"flask>=3.1.2",
"flask-cors>=6.0.1",
"gunicorn>=23.0.0",
"markdown>=3.9",
"pillow>=12.0.0",
"pydantic>=2.12.3",
"pygments>=2.19.2",
"python-dateutil>=2.9.0.post0",
"python-dotenv>=1.2.1",
"qrcode>=8.2",
"requests>=2.32.5",
"solana>=0.36.9",
"solders>=0.26.0",
]

View File

@@ -1,17 +0,0 @@
pydantic
flask
Flask-Cors
python-dotenv
gunicorn
requests
cloudflare
qrcode
Pillow
ansi2html
cachetools
solana
solders
weasyprint
markdown
pygments
beautifulsoup4

1026
server.py

File diff suppressed because it is too large Load Diff

53
templates/403.html Normal file
View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Nathan.Woodburn/</title>
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/403">
<meta property="og:url" content="https://nathan.woodburn.au/403">
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:title" content="Nathan.Woodburn/">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website">
<meta name="twitter:title" content="Nathan.Woodburn/">
<meta property="og:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta name="description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/404.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body>
<p>HTTP:&nbsp;<span>403</span></p>
<div class="text-center">
<div class="text-start" style="display: inline-block;"><code><em>this_page</em>.<em>found</em>&nbsp;= true;</code><code><span>if</span>&nbsp;(<em>this_page</em>.<em>readable</em>) {<br><span class="tab-space"></span><b>return</b> <em>this_page</em>;<br>}&nbsp;<span>else</span> {<br><span class="tab-space"></span><b>alert</b>('<i>This page is not readable!</i>');<br>}</code></div>
</div>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/403.min.js"></script>
</body>
</html>

View File

@@ -108,7 +108,7 @@
</div>
</div>
</section>
<p style="margin-top: 1em;">Hi, I am Nathan Woodburn and I live in Canberra<br>I am currently studying at the Australian National University<br>I enjoy 3D printing and CAD<br>I code stuff with C#, Linux Bash and tons of other languages<br>I'm a co-founder of <a href="https://hns.au" target="_blank">Handshake Australia</a><br>I currently work for <a href="https://learn.namebase.io" target="_blank">Namebase</a><br><br></p><i class="fas fa-arrow-down" style="font-size: 50px;" onclick="slideout()"></i>
<p style="margin-top: 1em;">Hi, I am Nathan Woodburn and I live in Canberra<br>I am currently studying at the Australian National University<br>I enjoy managing linux servers for my various projects<br>I code stuff with C#, Linux Bash and tons of other languages<br>I'm a co-founder of <a href="https://hns.au" target="_blank">Handshake Australia</a><br><br></p><i class="fas fa-arrow-down" style="font-size: 50px;" onclick="slideout()"></i>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>

View File

@@ -1 +1 @@
.name-container{display:inline-flex;align-items:center;overflow:hidden;position:absolute;width:fit-content;left:50%;transform:translateX(-50%)}.slider{position:relative;left:0;animation:1s linear 1s forwards slide}@keyframes slide{0%{left:0}100%{left:calc(100%)}}.brand{mask-image:linear-gradient(to right,black 50%,transparent 50%);-webkit-mask-image:linear-gradient(to right,black 50%,transparent 50%);mask-position:100% 0;-webkit-mask-position:100% 0;mask-size:200%;-webkit-mask-size:200%;animation:1s linear 1s forwards reveal}@keyframes reveal{0%{mask-position:100% 0;-webkit-mask-position:100% 0}100%{mask-position:0 0;-webkit-mask-position:0 0}}
.name-container{display:inline-flex;align-items:center;overflow:hidden;position:absolute;width:fit-content;left:50%;transform:translateX(-50%)}.slider{position:relative;left:0;animation:1s linear 1s forwards slide}@keyframes slide{0%{left:0}100%{left:calc(100%)}}.brand{mask-image:linear-gradient(to right,black 50%,transparent 50%);-webkit-mask-image:linear-gradient(to right,black 50%,transparent 50%);mask-position:100% 0;-webkit-mask-position:100% 0;mask-size:200%;-webkit-mask-size:200%;animation:1s linear 1s forwards reveal}@keyframes reveal{0%{mask-position:100% 0;-webkit-mask-position:100% 0}100%{mask-position:0 0;-webkit-mask-position:0 0}}.now-playing{position:fixed;bottom:0;right:0;border-top-left-radius:10px;background:#10101039;padding:1em}.hr-l{width:80%;border-width:2px;border-color:var(--bs-light);margin-top:0;opacity:.8}.hr-l-primary{border-width:3px;border-color:var(--bs-primary);margin-top:0;opacity:1}.float-right{position:absolute;right:3em}

View File

@@ -0,0 +1,104 @@
/* print.css */
@media print {
/* Page margins */
@page {
size: A4;
margin: 10mm 10mm;
}
/* Reset body */
body, html {
margin: 0;
padding: 0;
font-size: 10pt; /* smaller for print */
line-height: 1.3;
background: #fff !important;
color: #000 !important;
}
/* Container adjustments */
.container-fluid, .resume-row, .resume-column {
padding: 0 !important;
margin: 0 !important;
box-sizing: border-box;
/* background: none !important; */
}
/* Flex layout for 33/66 split */
.resume-row {
display: flex !important;
flex-wrap: nowrap !important;
width: 100% !important;
}
.resume-column-left {
flex: 0 0 33.3333% !important;
max-width: 33.3333% !important;
padding-left: 5mm !important;
padding-right: 5mm !important;
color: #fff !important;
border: none !important;
break-inside: avoid !important;
page-break-inside: avoid !important;
}
.resume-column-left a {
color: #fff !important;
text-decoration: none !important;
}
.resume-column-right {
flex: 0 0 66.6667% !important;
max-width: 66.6667% !important;
padding-left: 5mm !important;
padding-right: 5mm !important;
background: #fff !important;
color: #000 !important;
border: none !important;
break-inside: avoid !important;
page-break-inside: avoid !important;
}
/* Images adjustments */
img {
max-width: 100% !important;
height: auto !important;
display: block;
margin: 10mm auto !important;
}
/* Text adjustments for print */
h1 { font-size: 14pt; margin-bottom: 3mm; }
h2 { font-size: 12pt; margin-bottom: 2mm; }
h3 { font-size: 11pt; margin-bottom: 2mm; }
h4 { font-size: 10pt; margin-bottom: 1mm; }
h5, h6 { font-size: 9pt; margin-bottom: 1mm; }
p, li, .r-body, .l-body { font-size: 10pt; line-height: 1.3; }
.title {
font-size: 36px !important;
}
.subtitle {
font-size: 18px !important;
}
.r-heading1 {
margin-top: 4mm !important;
}
/* Links as plain text */
a {
color: #000 !important;
text-decoration: none !important;
}
/* Avoid page breaks inside blocks */
.noprintbreak {
break-inside: avoid !important;
page-break-inside: avoid !important;
/* margin-bottom: 5mm !important; */
}
.r-body {
margin-bottom: 0 !important;
}
}

View File

@@ -1 +1 @@
.profile-container{height:170px;width:170px;z-index:2;left:10%}.title{position:absolute;margin-left:calc(100px);width:calc(100% - 100px);padding:1em;margin-top:-225px;z-index:0}.title>*{width:100%;margin-bottom:0}img.profile{left:10px;width:150px;position:absolute;aspect-ratio:1;transform:scale(1);transition:.5s;z-index:2}img.background2{left:0;width:170px!important;margin-top:-10px;pointer-events:none;z-index:1}img.foreground{border-radius:50%;pointer-events:none;z-index:3}img.background:hover,img.backgroundsml:hover{filter:blur(5px)}.spacer{height:100px}img.profilesml{width:150px;position:absolute;left:50%;margin-left:-85px;aspect-ratio:1;padding-top:calc(var(--s)/5);transform:scale(1);transition:.5s}img.foregroundsml{border-radius:50%;pointer-events:none}img.background2sml{width:170px!important;left:calc(50% - 10px);margin-top:-10px;pointer-events:none;z-index:0}print_text{color:#000!important}@media print{.noprintbreak{page-break-inside:avoid}*{color:#000;background-color:#fff}body{background-color:#fff}.hideprint{display:none}.print_text{color:#000!important}.profile-container{margin-top:10px!important}.r-heading1{font-size:16pt!important;margin-bottom:10px!important}.r-heading2{font-size:14pt!important}.r-heading3{font-size:12pt!important}.r-body,.r-small{font-size:10pt!important}.spacer{height:25px!important}}.r-heading1{margin-bottom:20px}.r-heading2{margin-bottom:0}.r-heading3{margin-bottom:.5em}@media (max-width:500px){.print_text{font-size:10px}}
img.profile-side{width:200px;aspect-ratio:1;z-index:2;border:6px solid #fff;margin:3em 0;border-radius:50%}.spacer{height:100px}.l-heading1,.l-heading2,.r-heading2{margin-bottom:0}.l-heading3,.r-heading3{margin-bottom:.5em}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{text-transform:none}.side-column{margin-top:2em}.noprintbreak{margin-bottom:1.5em}.resume-column-left{background:var(--bs-primary);padding-left:3em;padding-right:3em;max-width:320px}.resume-column-right{padding-right:3em;padding-left:3em;background:var(--bs-light);color:var(--bs-black)}.row-fill div{padding:0}.r-heading1{font-size:28px;margin-bottom:0;color:var(--bs-primary)}.title-hr{width:15%;color:var(--bs-primary);border-width:5px;border-color:var(--bs-primary);opacity:1}.l-body{margin-left:1em;line-height:initial}.r-body{line-height:initial}.l-summary{margin-top:3em}::selection{color:#fff;background-color:#0c4279}

View File

@@ -1 +1 @@
:root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}@media (min-width:992px){.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}}
:root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}

1
templates/assets/css/tools.min.css vendored Normal file
View File

@@ -0,0 +1 @@
.card:hover{transform:translateY(-5px);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);transition:transform .2s,box-shadow .2s}.btn:hover{transform:scale(1.05);transition:transform .2s}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
templates/assets/js/403.min.js vendored Normal file
View File

@@ -0,0 +1 @@
const trigger="s",secret="/supersecretpath",home="/";var isSecret=!1;function error_check(){return function(){isSecret?(alert("You found the secret path"),window.location=secret):(alert("This page is not readable!"))}}function type(e,t){var n=document.getElementsByTagName("code")[e].innerHTML.toString(),o=0;document.getElementsByTagName("code")[e].innerHTML="",setTimeout((function(){var t=setInterval((function(){o++,document.getElementsByTagName("code")[e].innerHTML=n.slice(0,o)+"|",o==n.length&&(clearInterval(t),document.getElementsByTagName("code")[e].innerHTML=n)}),10)}),t)}setTimeout(error_check(),5e3),document.addEventListener("keydown",(function(e){"s"==e.key&&(isSecret=!0)})),type(0,0),type(1,600),type(2,1300);

View File

@@ -1,446 +0,0 @@
;(function() {
"use strict";
// General
var canvas,
screen,
gameSize,
game;
// Assets
var invaderCanvas,
invaderMultiplier,
invaderSize = 20,
initialOffsetInvader,
invaderAttackRate,
invaderSpeed,
invaderSpawnDelay = 250;
// Counter
var i = 0,
kills = 0,
spawnDelayCounter = invaderSpawnDelay;
var invaderDownTimer;
// Text
var blocks = [
[3, 4, 8, 9, 10, 15, 16],
[2, 4, 7, 11, 14, 16],
[1, 4, 7, 11, 13, 16],
[1, 2, 3, 4, 5, 7, 11, 13, 14, 15, 16, 17],
[4, 7, 11, 16],
[4, 8, 9, 10, 16]
];
// Game Controller
// ---------------
var Game = function() {
this.level = -1;
this.lost = false;
this.player = new Player();
this.invaders = [];
this.invaderShots = [];
if (invaderDownTimer === undefined) {
invaderDownTimer = setInterval(function() {
for (i = 0; i < game.invaders.length; i++) game.invaders[i].move();
}, 1000 - (this.level * 1.8));
};
}
Game.prototype = {
update: function() {
// Next level
if (game.invaders.length === 0) {
spawnDelayCounter += 1;
if (spawnDelayCounter < invaderSpawnDelay) return;
this.level += 1;
invaderAttackRate -= 0.002;
invaderSpeed += 10;
game.invaders = createInvaders();
spawnDelayCounter = 0;
}
if (!this.lost) {
// Collision
game.player.projectile.forEach(function(projectile) {
game.invaders.forEach(function(invader) {
if (collides(projectile, invader)) {
invader.destroy();
projectile.active = false;
}
});
});
this.invaderShots.forEach(function(invaderShots) {
if (collides(invaderShots, game.player)) {
game.player.destroy();
}
});
for (i = 0; i < game.invaders.length; i++) game.invaders[i].update();
}
// Don't stop player & projectiles.. they look nice
game.player.update();
for (i = 0; i < game.invaderShots.length; i++) game.invaderShots[i].update();
this.invaders = game.invaders.filter(function(invader) {
return invader.active;
});
},
draw: function() {
if (this.lost) {
screen.fillStyle = "rgba(0, 0, 0, 0.03)";
screen.fillRect(0, 0, gameSize.width, gameSize.height);
screen.font = "55px Lucida Console";
screen.textAlign = "center";
screen.fillStyle = "white";
screen.fillText("You lost", gameSize.width / 2, gameSize.height / 2);
screen.font = "20px Lucida Console";
screen.fillText("Points: " + kills, gameSize.width / 2, gameSize.height / 2 + 30);
} else {
screen.clearRect(0, 0, gameSize.width, gameSize.height);
screen.font = "10px Lucida Console";
screen.textAlign = "right";
screen.fillText("Points: " + kills, gameSize.width, gameSize.height - 12);
}
screen.beginPath();
var i;
this.player.draw();
if (!this.lost)
for (i = 0; i < this.invaders.length; i++) this.invaders[i].draw();
for (i = 0; i < this.invaderShots.length; i++) this.invaderShots[i].draw();
screen.fill();
},
invadersBelow: function(invader) {
return this.invaders.filter(function(b) {
return Math.abs(invader.coordinates.x - b.coordinates.x) === 0 &&
b.coordinates.y > invader.coordinates.y;
}).length > 0;
}
};
// Invaders
// --------
var Invader = function(coordinates) {
this.active = true;
this.coordinates = coordinates;
this.size = {
width: invaderSize,
height: invaderSize
};
this.patrolX = 0;
this.speedX = invaderSpeed;
};
Invader.prototype = {
update: function() {
if (Math.random() > invaderAttackRate && !game.invadersBelow(this)) {
var projectile = new Projectile({
x: this.coordinates.x + this.size.width / 2,
y: this.coordinates.y + this.size.height - 5
}, {
x: 0,
y: 2
});
game.invaderShots.push(projectile);
}
},
draw: function() {
if (this.active) screen.drawImage(invaderCanvas, this.coordinates.x, this.coordinates.y);
},
move: function() {
if (this.patrolX < 0 || this.patrolX > 100) {
this.speedX = -this.speedX;
this.patrolX += this.speedX;
this.coordinates.y += this.size.height;
if (this.coordinates.y + this.size.height * 2 > gameSize.height) game.lost = true;
} else {
this.coordinates.x += this.speedX;
this.patrolX += this.speedX;
}
},
destroy: function() {
this.active = false;
kills += 1;
}
};
// Player
// ------
var Player = function() {
this.active = true;
this.size = {
width: 16,
height: 8
};
this.shooterHeat = -3;
this.coordinates = {
x: gameSize.width / 2 - (this.size.width / 2) | 0,
y: gameSize.height - this.size.height * 2
};
this.projectile = [];
this.keyboarder = new KeyController();
};
Player.prototype = {
update: function() {
for (var i = 0; i < this.projectile.length; i++) this.projectile[i].update();
this.projectile = this.projectile.filter(function(projectile) {
return projectile.active;
});
if (!this.active) return;
if (this.keyboarder.isDown(this.keyboarder.KEYS.LEFT) && this.coordinates.x > 0) this.coordinates.x -= 2;
else if (this.keyboarder.isDown(this.keyboarder.KEYS.RIGHT) && this.coordinates.x < gameSize.width - this.size.width) this.coordinates.x += 2;
if (this.keyboarder.isDown(this.keyboarder.KEYS.Space)) {
this.shooterHeat += 1;
if (this.shooterHeat < 0) {
var projectile = new Projectile({
x: this.coordinates.x + this.size.width / 2 - 1,
y: this.coordinates.y - 1
}, {
x: 0,
y: -7
});
this.projectile.push(projectile);
} else if (this.shooterHeat > 12) this.shooterHeat = -3;
} else {
this.shooterHeat = -3;
}
},
draw: function() {
if (this.active) {
screen.rect(this.coordinates.x, this.coordinates.y, this.size.width, this.size.height);
screen.rect(this.coordinates.x - 2, this.coordinates.y + 2, 20, 6);
screen.rect(this.coordinates.x + 6, this.coordinates.y - 4, 4, 4);
}
for (var i = 0; i < this.projectile.length; i++) this.projectile[i].draw();
},
destroy: function() {
this.active = false;
game.lost = true;
}
};
// Projectile
// ------
var Projectile = function(coordinates, velocity) {
this.active = true;
this.coordinates = coordinates;
this.size = {
width: 3,
height: 3
};
this.velocity = velocity;
};
Projectile.prototype = {
update: function() {
this.coordinates.x += this.velocity.x;
this.coordinates.y += this.velocity.y;
if (this.coordinates.y > gameSize.height || this.coordinates.y < 0) this.active = false;
},
draw: function() {
if (this.active) screen.rect(this.coordinates.x, this.coordinates.y, this.size.width, this.size.height);
}
};
// Keyboard input tracking
// -----------------------
var KeyController = function() {
this.KEYS = {
LEFT: 37,
RIGHT: 39,
Space: 32
};
var keyCode = [37, 39, 32];
var keyState = {};
var counter;
window.addEventListener('keydown', function(e) {
for (counter = 0; counter < keyCode.length; counter++)
if (keyCode[counter] == e.keyCode) {
keyState[e.keyCode] = true;
e.preventDefault();
}
});
window.addEventListener('keyup', function(e) {
for (counter = 0; counter < keyCode.length; counter++)
if (keyCode[counter] == e.keyCode) {
keyState[e.keyCode] = false;
e.preventDefault();
}
});
this.isDown = function(keyCode) {
return keyState[keyCode] === true;
};
};
// Other functions
// ---------------
function collides(a, b) {
return a.coordinates.x < b.coordinates.x + b.size.width &&
a.coordinates.x + a.size.width > b.coordinates.x &&
a.coordinates.y < b.coordinates.y + b.size.height &&
a.coordinates.y + a.size.height > b.coordinates.y;
}
function getPixelRow(rowRaw) {
var textRow = [],
placer = 0,
row = Math.floor(rowRaw / invaderMultiplier);
if (row >= blocks.length) return [];
for (var i = 0; i < blocks[row].length; i++) {
var tmpContent = blocks[row][i] * invaderMultiplier;
for (var j = 0; j < invaderMultiplier; j++) textRow[placer + j] = tmpContent + j;
placer += invaderMultiplier;
}
return textRow;
}
// Write Text
// -----------
function createInvaders() {
var invaders = [];
var i = blocks.length * invaderMultiplier;
while (i--) {
var j = getPixelRow(i);
for (var k = 0; k < j.length; k++) {
invaders.push(new Invader({
x: j[k] * invaderSize,
y: i * invaderSize
}));
}
}
return invaders;
}
// Start game
// ----------
window.addEventListener('load', function() {
var invaderAsset = new Image;
invaderAsset.onload = function() {
invaderCanvas = document.createElement('canvas');
invaderCanvas.width = invaderSize;
invaderCanvas.height = invaderSize;
invaderCanvas.getContext("2d").drawImage(invaderAsset, 0, 0);
// Game Creation
canvas = document.getElementById("space-invaders");
screen = canvas.getContext('2d');
initGameStart();
loop();
};
invaderAsset.src = "/assets/img/invader.gif";
});
window.addEventListener('resize', function() {
initGameStart();
});
document.getElementById('restart').addEventListener('click', function() {
initGameStart();
});
function initGameStart() {
if (window.innerWidth > 1200) {
screen.canvas.width = 1200;
screen.canvas.height = 500;
gameSize = {
width: 1200,
height: 500
};
invaderMultiplier = 3;
initialOffsetInvader = 420;
} else if (window.innerWidth > 800) {
screen.canvas.width = 900;
screen.canvas.height = 600;
gameSize = {
width: 900,
height: 600
};
invaderMultiplier = 2;
initialOffsetInvader = 280;
} else {
screen.canvas.width = 600;
screen.canvas.height = 300;
gameSize = {
width: 600,
height: 300
};
invaderMultiplier = 1;
initialOffsetInvader = 140;
}
kills = 0;
invaderAttackRate = 0.999;
invaderSpeed = 20;
spawnDelayCounter = invaderSpawnDelay;
game = new Game();
}
function loop() {
game.update();
game.draw();
requestAnimationFrame(loop);
}
})();

View File

@@ -1,154 +0,0 @@
document.addEventListener("DOMContentLoaded", function() {
const loadingScreen = document.getElementById("loading-screen");
// Terminal simulation data
const commands = [
{ pre: '┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~</span>]', message: "cd Git" },
{ pre: '┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~/Git</span>]', message: "cd Nathanwoodburn.github.io" },
{ pre: '┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~/Git/Nathanwoodburn.github.io</span>]', message: "python3 main.py" }
];
const serverMessages = [
"Starting server with 1 workers and 2 threads",
"+0000] [1] [INFO] Starting gunicorn 22.0.0",
"+0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)",
"+0000] [1] [INFO] Using worker: gthread",
"+0000] [8] [INFO] Booting worker with pid: 8",
"Preloading assets for faster navigation..."
];
let currentCommand = 0;
let assetsLoaded = false;
let terminalComplete = false;
// Enhanced asset preloading function
function preloadAssets() {
const assets = [
// Additional CSS files that might not be in preload
'/assets/css/animate.min.min.css',
'/assets/css/fixes.min.css',
'/assets/css/Footer-Dark-icons.min.css',
'/assets/css/GridSystem-1.min.css',
// Font files
'/assets/fonts/fa-solid-900.woff2',
'/assets/fonts/fa-brands-400.woff2',
'/assets/fonts/fa-regular-400.woff2'
];
let loadedCount = 0;
const totalAssets = assets.length;
function onAssetLoad() {
loadedCount++;
if (loadedCount === totalAssets) {
assetsLoaded = true;
checkReadyToRedirect();
}
}
// Load additional assets
assets.forEach(assetUrl => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = assetUrl.endsWith('.css') ? 'style' :
assetUrl.endsWith('.js') ? 'script' :
assetUrl.includes('/fonts/') ? 'font' : 'fetch';
if (link.as === 'font') {
link.crossOrigin = 'anonymous';
}
link.href = assetUrl;
link.onload = onAssetLoad;
link.onerror = onAssetLoad; // Count errors as loaded to prevent hanging
document.head.appendChild(link);
});
// If no additional assets, mark as loaded
if (totalAssets === 0) {
assetsLoaded = true;
checkReadyToRedirect();
}
}
function checkReadyToRedirect() {
if (assetsLoaded && terminalComplete) {
setTimeout(redirectToIndex, 200);
}
}
function getCurrentTime() {
const now = new Date();
return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')} ${String(now.getUTCHours()).padStart(2, '0')}:${String(now.getUTCMinutes()).padStart(2, '0')}:${String(now.getUTCSeconds()).padStart(2, '0')}`;
}
function displayServerMessages(messages, callback) {
const timestamp = getCurrentTime();
for (let i = 0; i < messages.length; i++) {
const messageDiv = document.createElement("div");
messageDiv.classList.add("loading-line");
messageDiv.innerHTML = i !== 0 ? "[" + timestamp + "] " + messages[i] : messages[i];
loadingScreen.appendChild(messageDiv);
}
callback();
}
function redirectToIndex() {
if (window.location.pathname === "/") {
window.location.reload();
} else {
window.location.href = "/";
}
}
// Event listeners for manual redirect
window.addEventListener("keypress", redirectToIndex);
if (window.innerWidth < 768) {
console.log("Screen width is less than 768px, allowing click to redirect");
window.addEventListener("click", redirectToIndex);
}
function typeCommand(command, callback) {
const preDiv = document.createElement("div");
preDiv.classList.add("loading-pre");
preDiv.innerHTML = command.pre;
loadingScreen.appendChild(preDiv);
const commandDiv = document.createElement("div");
commandDiv.classList.add("loading-line");
commandDiv.innerHTML = '└─<span class="blue">$</span> <span class="cursor"></span>';
loadingScreen.appendChild(commandDiv);
let charIndex = 0;
const typeInterval = setInterval(() => {
commandDiv.removeChild(commandDiv.querySelector(".cursor"));
commandDiv.innerHTML += command.message[charIndex] + '<span class="cursor"></span>';
charIndex++;
if (charIndex === command.message.length) {
commandDiv.removeChild(commandDiv.querySelector(".cursor"));
clearInterval(typeInterval);
callback();
}
}, 50);
}
function runTerminalSimulation() {
if (currentCommand < commands.length) {
typeCommand(commands[currentCommand], () => {
currentCommand++;
setTimeout(runTerminalSimulation, 200);
});
} else {
displayServerMessages(serverMessages, () => {
terminalComplete = true;
checkReadyToRedirect();
});
}
}
// Start the loading process
preloadAssets();
runTerminalSimulation();
});

View File

@@ -53,7 +53,9 @@ Find something interesting to read. Or maybe check one of my tutorials">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -54,7 +54,9 @@ Find something interesting to read. Or maybe check one of my tutorials">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

14
templates/contact.ascii Normal file
View File

@@ -0,0 +1,14 @@
{{header}}
───────────────────────────────────────────────
 CONTACT ME 
────────────
Here are my socials — Im most active on Discord 💬
- Twitter: https://twitter.com/woodburn_nathan
- GitHub: https://github.com/Nathanwoodburn
- Email: mailto:about@nathan.woodburn.au
- Discord: https://l.woodburn.au/discord
- Mastodon: https://mastodon.woodburn.au/@nathanwoodburn
- YouTube: https://www.youtube.com/@nathanjwoodburn

25
templates/donate.ascii Normal file
View File

@@ -0,0 +1,25 @@
{{header}}
───────────────────────────────────────────────
 DONATE 
────────
If youd like to support my work 💙
- PayPal: https://paypal.me/nathanwoodburn
- GitHub: https://github.com/sponsors/Nathanwoodburn
- Stripe: https://donate.stripe.com/8wM6pv0VD08Xe408ww
HNS: nathan.woodburn
{{ HNS }}
BTC: thinbadger6@primal.net
{{ BTC }}
SOL: woodburn.sol
{{ SOL }}
ETH: woodburn.au
{{ ETH }}
More donation options → [/donate/more]

View File

@@ -51,7 +51,9 @@
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -0,0 +1,10 @@
{{header}}
───────────────────────────────────────────────
 DONATE 
────────
Here is my {{ coin }} address if you'd like to send a donation 💙
{{ address }}
Thank you for your support! 🙏

View File

@@ -0,0 +1,13 @@
{{header}}
───────────────────────────────────────────────
 DONATE 
────────
Here is a list of additional cryptocurrencies and donation methods 💙
For each coin below, you can get the address from /donate/<coin>
{% for coin in coins %}{% if loop.index0 % 4 == 0 and loop.index0 != 0 %}
{% endif %}{{ coin }}{% if not loop.last %}, {% endif %}{% endfor %}
Thank you for your support! 🙏

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

25
templates/favicon.ascii Normal file
View File

@@ -0,0 +1,25 @@
▒▒▒ ▓▓▓
▒░░░░▒▓ ▓▓▓▓▓▓▓
▒░░░░░░▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓
▒░░░░░▒▒▒▒▒▒▒ ▓▓▒▓▓▓▓▓▓▓▓▓▓
▒░░░▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒░░▒▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒ ▒▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒ ▒▒▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒▒▒ ▒▒▒▒▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒▒▒▒▒▒ ▒▒▒▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒▒▒▒▒▒▒▒ ▒▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█
▓▒▒▒▒▒▒▒▒▒▒▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▒▒▒▒▓▓▓ ▓▓▓▓▓▓▓▓█
▓▓▓▓ ▓▓▓█

13
templates/header.ascii Normal file
View File

@@ -0,0 +1,13 @@
─────────────────────────────────────────────────────
 . . , . . . .. / 
 |\ | _.-+-|_ _.._ | | _ _ _||_ . .._.._ / 
 | \|(_] | [ )(_][ ) * |/\|(_)(_)(_][_)(_|[ [ )/ 
─────────────────────────────────────────────────────
Home [/]
Contact [/contact]
Projects [/projects]
Tools [/tools]
Donate [/donate]
Now [/now]

391
templates/hosting.html Normal file
View File

@@ -0,0 +1,391 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en-au" style="height: 100%;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Nathan.Woodburn/</title>
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/hosting">
<meta property="og:url" content="https://nathan.woodburn.au/hosting">
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:title" content="Nathan.Woodburn/">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website">
<meta name="twitter:title" content="Nathan.Woodburn/">
<meta property="og:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta name="description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/font-awesome.min.css">
<link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="77" style="display: flex;flex-direction: column;">{{handshake_scripts | safe}}
<nav class="navbar navbar-expand-md fixed-top navbar-light" id="mainNav" style="background: var(--bs-navbar-hover-color);">
<div class="container-fluid"><a class="navbar-brand" href="/#">
<div style="padding-right: 1em;display: inline-flex;">
<div class="slider"><span>/</span></div><span class="brand">Nathan.Woodburn</span>
</div>
</a><button data-bs-toggle="collapse" class="navbar-toggler navbar-toggler-right" data-bs-target="#navbarResponsive" type="button" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation" value="Menu"><i class="fa fa-bars"></i></button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>
</div>
</div>
</nav>
<section class="text-center content-section" style="background: #110033;padding-bottom: 100px;flex: 1;display: flex;flex-direction: column;justify-content: center;">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<h2>Web Hosting</h2>
<p style="margin-bottom: 10px;">Configure your hosting setup below</p><form id="costForm">
<div style="padding: 10px;">
<label style="font-weight: bold; margin-bottom: 15px; display: block;">Choose a plan:</label>
<div style="display: flex; gap: 15px; flex-wrap: wrap; margin-top: 10px;">
<div class="plan-card" onclick="selectPreset('standard')" style="border: 2px solid #ddd; border-radius: 8px; padding: 20px; cursor: pointer; flex: 1; min-width: 250px; background: white; transition: all 0.3s ease;">
<input type="radio" name="preset" value="standard" onchange="handlePresetChange()" checked style="margin-bottom: 10px;">
<h4 style="margin: 0 0 10px 0; color: #333;">Standard WordPress</h4>
<div style="font-size: 24px; font-weight: bold; color: #007bff; margin-bottom: 10px;">$6/month</div>
<ul style="list-style: none; padding: 0; margin: 0; color: #666;">
<li style="margin-bottom: 5px;">✓ 1 CPU Core</li>
<li style="margin-bottom: 5px;">✓ 0.5GB RAM</li>
<li style="margin-bottom: 5px;">✓ 10GB Storage</li>
<li style="margin-bottom: 5px;">✓ Perfect for small sites</li>
</ul>
</div>
<div class="plan-card" onclick="selectPreset('premium')" style="border: 2px solid #ddd; border-radius: 8px; padding: 20px; cursor: pointer; flex: 1; min-width: 250px; background: white; transition: all 0.3s ease;">
<input type="radio" name="preset" value="premium" onchange="handlePresetChange()" style="margin-bottom: 10px;">
<h4 style="margin: 0 0 10px 0; color: #333;">Premium WordPress</h4>
<div style="font-size: 24px; font-weight: bold; color: #28a745; margin-bottom: 10px;">$10/month</div>
<ul style="list-style: none; padding: 0; margin: 0; color: #666;">
<li style="margin-bottom: 5px;">✓ 1 CPU Core</li>
<li style="margin-bottom: 5px;">✓ 1GB RAM</li>
<li style="margin-bottom: 5px;">✓ 50GB Storage</li>
<li style="margin-bottom: 5px;">✓ Weekly Backups Included</li>
</ul>
</div>
<div class="plan-card" onclick="selectPreset('custom')" style="border: 2px solid #ddd; border-radius: 8px; padding: 20px; cursor: pointer; flex: 1; min-width: 250px; background: white; transition: all 0.3s ease;">
<input type="radio" name="preset" value="custom" onchange="handlePresetChange()" style="margin-bottom: 10px;">
<h4 style="margin: 0 0 10px 0; color: #333;">Custom Configuration</h4>
<div style="font-size: 24px; font-weight: bold; color: #6c757d; margin-bottom: 10px;">Variable</div>
<ul style="list-style: none; padding: 0; margin: 0; color: #666;">
<li style="margin-bottom: 5px;">✓ Choose your CPU</li>
<li style="margin-bottom: 5px;">✓ Choose your RAM</li>
<li style="margin-bottom: 5px;">✓ Choose your Storage</li>
<li style="margin-bottom: 5px;">✓ Optional Backups</li>
</ul>
</div>
</div>
</div>
<div id="customSliders" style="display: none;">
<div>
<label class="form-label" for="cpus" style="padding: 10px;">CPUs:
<span id="cpusValue" style="display:inline-block; min-width: 3ch; font-family: monospace;">1</span>
</label>
<input id="cpus" name="cpus" class="form-range" type="range"
min="1" max="4" value="1" step="1"
style="max-width: 500px; padding-top: 10px;"
oninput="updateValue('cpus')">
</div>
<div>
<label class="form-label" for="memory" style="padding: 10px;">Memory (GB):
<span id="memoryValue" style="display:inline-block; min-width: 5ch; font-family: monospace;">0.5</span>
</label>
<input id="memory" name="memory" class="form-range" type="range"
min="0" max="6" value="0" step="1"
style="max-width: 500px; padding-top: 10px;"
oninput="updateValue('memory')">
</div>
<div>
<label class="form-label" for="disk" style="padding: 10px;">Disk Space (GB):
<span id="diskValue" style="display:inline-block; min-width: 5ch; font-family: monospace;">10</span>
</label>
<input id="disk" name="disk" class="form-range" type="range"
min="10" max="500" value="10" step="10"
style="max-width: 500px; padding-top: 10px;"
oninput="updateValue('disk')">
</div>
<div style="padding: 10px;">
<label><input type="checkbox" id="backups" onchange="calculateCost()"> Backups ($5/month for up to 150GB)</label>
</div>
</div>
<div style="padding: 10px; font-weight: bold;">
Monthly Cost: $<span id="monthlyCost" style="display:inline-block; min-width: 6ch; font-family: monospace; text-align: left;">0.00</span><br>
<small>All prices are in AUD</small>
<br>
<input type="email" id="email" name="email" class="form-control" placeholder="Your Email" required style="margin-top: 10px;">
<!-- Allow message -->
<textarea id="message" name="message" class="form-control" placeholder="Your Message" rows="3" style="margin-top: 10px;"></textarea>
<button type="button" class="btn btn-primary" style="margin-top: 10px;" onclick="sendEnquiry()">Send Enquiry</button>
<small id="enquiryStatus" style="display: block; margin-top: 10px;"></small>
</div>
</form>
<script>
// Rates
const cpuRate = 3;
const memoryRate = 5;
const diskRate = 0.1;
const backupRate = 1;
// Memory values mapping
const memoryValues = [0.5, 1, 2, 4, 8, 16, 24];
// Initialize on load
document.addEventListener('DOMContentLoaded', () => {
// Check if parameters are in the URL
const urlParams = new URLSearchParams(window.location.search);
// Check for preset parameter
if (urlParams.has('preset')) {
const presetValue = urlParams.get('preset');
const presetRadio = document.querySelector(`input[name="preset"][value="${presetValue}"]`);
if (presetRadio) {
presetRadio.checked = true;
handlePresetChange();
}
}
if (urlParams.has('cpus')) {
document.getElementById('cpus').value = urlParams.get('cpus');
}
if (urlParams.has('memory')) {
const memoryParam = parseFloat(urlParams.get('memory'));
const memoryIndex = memoryValues.indexOf(memoryParam);
document.getElementById('memory').value = memoryIndex >= 0 ? memoryIndex : 0;
}
if (urlParams.has('disk')) {
document.getElementById('disk').value = urlParams.get('disk');
}
if (urlParams.has('backups')) {
document.getElementById('backups').checked = urlParams.get('backups') === 'true';
}
if (urlParams.has('email')) {
document.getElementById('email').value = urlParams.get('email');
}
if (urlParams.has('message')) {
document.getElementById('message').value = urlParams.get('message');
}
// Update the displayed values
updateValue('cpus');
updateValue('memory');
updateValue('disk');
calculateCost();
// Send the enquiry if the form is submitted
document.getElementById('costForm').addEventListener('submit', function(event) {
event.preventDefault();
sendEnquiry();
});
// Initialize preset handling and card styles
handlePresetChange();
updateCardStyles();
});
function updateValue(field) {
let value = document.getElementById(field).value;
if (field === 'memory') {
value = memoryValues[parseInt(value)];
}
document.getElementById(field + 'Value').textContent = value;
calculateCost();
}
function selectPreset(presetValue) {
document.querySelector(`input[name="preset"][value="${presetValue}"]`).checked = true;
handlePresetChange();
updateCardStyles();
}
function updateCardStyles() {
const cards = document.querySelectorAll('.plan-card');
const selectedPreset = document.querySelector('input[name="preset"]:checked').value;
cards.forEach((card, index) => {
const presetValues = ['standard', 'premium', 'custom'];
const isSelected = presetValues[index] === selectedPreset;
if (isSelected) {
card.style.borderColor = '#007bff';
card.style.background = '#f8f9fa';
card.style.transform = 'translateY(-2px)';
card.style.boxShadow = '0 4px 12px rgba(0,123,255,0.15)';
} else {
card.style.borderColor = '#ddd';
card.style.background = 'white';
card.style.transform = 'translateY(0)';
card.style.boxShadow = 'none';
}
});
}
function handlePresetChange() {
const selectedPreset = document.querySelector('input[name="preset"]:checked').value;
const customSliders = document.getElementById('customSliders');
updateCardStyles();
if (selectedPreset === 'custom') {
customSliders.style.display = 'block';
} else {
customSliders.style.display = 'none';
// Set preset values
if (selectedPreset === 'standard') {
document.getElementById('cpus').value = 1;
document.getElementById('memory').value = 0; // 0.5GB
document.getElementById('disk').value = 10;
document.getElementById('backups').checked = false;
} else if (selectedPreset === 'premium') {
document.getElementById('cpus').value = 1;
document.getElementById('memory').value = 1; // 1GB
document.getElementById('disk').value = 50;
document.getElementById('backups').checked = true;
}
// Update displayed values
updateValue('cpus');
updateValue('memory');
updateValue('disk');
}
calculateCost();
}
function calculateCost() {
const selectedPreset = document.querySelector('input[name="preset"]:checked').value;
// For presets, use fixed pricing
if (selectedPreset === 'standard') {
document.getElementById('monthlyCost').textContent = '6.00';
return;
} else if (selectedPreset === 'premium') {
document.getElementById('monthlyCost').textContent = '10.00';
return;
}
// Custom calculation
const cpus = parseFloat(document.getElementById('cpus').value);
const memoryIndex = parseInt(document.getElementById('memory').value);
const memory = memoryValues[memoryIndex];
const disk = parseFloat(document.getElementById('disk').value);
let monthlyCost = (cpus * cpuRate) + (memory * memoryRate) + (disk * diskRate);
if (document.getElementById('backups').checked) {
const backupUnits = Math.ceil(disk / 50);
monthlyCost += backupUnits * backupRate;
}
document.getElementById('monthlyCost').textContent = monthlyCost.toFixed(2);
}
function sendEnquiry() {
// Ensure that the email field is filled
const email = document.getElementById('email').value;
if (!email) {
document.getElementById('enquiryStatus').textContent = 'Please enter your email address.';
document.getElementById('enquiryStatus').style.color = 'red';
return;
}
// Collect form data
const selectedPreset = document.querySelector('input[name="preset"]:checked').value;
const cpus = document.getElementById('cpus').value;
const memoryIndex = parseInt(document.getElementById('memory').value);
const memory = memoryValues[memoryIndex];
const disk = document.getElementById('disk').value;
const backups = document.getElementById('backups').checked ? 'Yes' : 'No';
const message = document.getElementById('message').value;
const enquiryData = {
email: email,
preset: selectedPreset,
cpus: cpus,
memory: memory,
disk: disk,
backups: backups,
message: message
};
// Send the enquiry data to the server
fetch('/hosting/send-enquiry', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(enquiryData)
})
.then(response => {
if (response.ok) {
document.getElementById('enquiryStatus').textContent = 'Enquiry sent successfully!';
document.getElementById('enquiryStatus').style.color = 'green';
calculateCost();
} else {
document.getElementById('enquiryStatus').textContent = 'Failed to send enquiry. Please try again.';
document.getElementById('enquiryStatus').style.color = 'red';
}
})
.catch(error => {
console.error('Error sending enquiry:', error);
document.getElementById('enquiryStatus').textContent = 'Error sending enquiry. Please check your network connection.';
document.getElementById('enquiryStatus').style.color = 'red';
});
}
</script>
</div>
</div>
</div>
</section>
<footer style="background: #110033;">
<div class="container text-center">
<div class="row">
<div class="col">
<p class="copyright">Copyright ©&nbsp;Nathan.Woodburn/ 2025</p>
</div>
</div>
</div>
</footer>{{custom | safe}}
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/hacker.min.js"></script>
</body>
</html>

46
templates/index.ascii Normal file
View File

@@ -0,0 +1,46 @@
─────────────────────────────────────────────────────
 . . , . . . .. / 
 |\ | _.-+-|_ _.._ | | _ _ _||_ . .._.._ / 
 | \|(_] | [ )(_][ ) * |/\|(_)(_)(_][_)(_|[ [ )/ 
─────────────────────────────────────────────────────
Home [/]
Contact [/contact]
Projects [/projects]
Tools [/tools]
Donate [/donate]
Now [/now]
API [/api/v1]
───────────────────────────────────────────────
 ABOUT ME 
──────────
Hi, I'm Nathan Woodburn from Canberra, Australia.
I've been homeschooled through Year 12 and am now studying a
Bachelor of Computer Science.
I love building random projects, so this site is always evolving.
I'm also one of the founders of Handshake AU [https://hns.au],
working to grow Handshake adoption across Australia.
I'm currently working on: {{ repo | safe }}
{% if not spotify.message %}Currently listening to: {{ spotify.song_name }} by {{ spotify.artist }}{% endif %}
───────────────────────────────────────────────
 SKILLS 
────────
- Linux servers & CLI
- DNS & DNSSEC
- NGINX web servers
- Programming:
- Python 3
- C#
- Java
- Bash
Served to: {{ ip }}
───────────────────────────────────────────────

View File

@@ -49,7 +49,7 @@
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="stylesheet" href="/assets/css/swiper.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script><link rel="preload" as="image" href="/assets/img/bg/BlueMountains.jpg" type="image/jpeg">
</head>
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="77"><script>
@@ -67,7 +67,9 @@
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>
@@ -77,7 +79,7 @@
<div class="text-end d-none d-xl-block d-xxl-block" id="downtime"><blockquote class="speech bubble"><em>G'day!</em><br>
Some services are down.<br>
Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.webp"></div>
<header class="masthead main" style="background: url(&quot;/assets/img/bg/BlueMountains.jpg&quot;) center / cover;position: relative;height: 400px;">
<header class="masthead main" style="position: relative;height: 400px;background: url(&quot;/assets/img/bg/BlueMountains.jpg&quot;) center / cover;">
<div class="intro-body text parallax">
<div class="name-container" style="padding-right: 1em;">
<div class="slider">
@@ -93,8 +95,8 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
<div class="row">
<div class="col-lg-8 mx-auto">
<h2>About ME</h2>
<div class="profile-container" style="margin-bottom: 2em;"><img class="profile background" src="/assets/img/profile.jpg" style="border-radius: 50%;" alt="My Profile"><img class="profile foreground" src="/assets/img/pfront.webp" alt=""></div>
<p style="margin-bottom: 5px;">Hi, I'm Nathan Woodburn and I live in Canberra, Australia.<br>I've been home schooled all the way to Yr 12.<br>I'm currently studying a&nbsp;Bachelor of Computer Science.<br>I create tons of random projects so this site is often behind.<br>I'm one of the founders of <a href="https://hns.au" target="_blank">Handshake AU</a>&nbsp;working to increase Handshake adoption in Australia.<br>I work for <a href="https://www.namebase.io" target="_blank">Namebase</a>&nbsp;as tech and general support. Namebase is a US based company owned by <a href="https://namecheap.com" target="_blank">Namecheap</a>.</p>
<div class="profile-container" style="margin-bottom: 2em;"><img class="profile background" src="/assets/img/profile.webp" style="border-radius: 50%;" alt="My Profile"><img class="profile foreground" src="/assets/img/pfront.webp" alt=""></div>
<p style="margin-bottom: 5px;">Hi, I'm Nathan Woodburn and I live in Canberra, Australia.<br>I've been home schooled all the way to Yr 12.<br>I'm currently studying a&nbsp;Bachelor of Computer Science.<br>I create tons of random projects so this site is often behind.<br>I'm one of the founders of <a href="https://hns.au" target="_blank">Handshake AU</a>&nbsp;working to increase Handshake adoption in Australia.</p>
<p title="{{repo_description}}" style="margin-bottom: 0px;display: inline-block;">I'm currently working on</p>
<p data-bs-toggle="tooltip" data-bss-tooltip="" title="{{repo_description}}" style="display: inline-block;">{{repo | safe}}</p>
</div>
@@ -103,12 +105,16 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
<div class="col-lg-8 mx-auto">
<h2>Skills</h2>
<ul class="list-unstyled" style="font-size: 18px;">
<li class="printing">3D Printing</li>
<li>Autodesk Fusion 360 (CAD Modeling)</li>
<li class="programc">Programming with various languages</li>
<li>DNS, DNSSEC and Trustless SSL</li>
<li class="programlinux">Linux Servers and CLI</li>
<li>DNS and DNSSEC</li>
<li class="programnginx">NGINX Web Servers</li>
<li class="programc">Programming in<ul class="list-inline">
<li class="list-inline-item">Python 3</li>
<li class="list-inline-item">C#</li>
<li class="list-inline-item">Java</li>
<li class="list-inline-item">Bash</li>
</ul>
</li>
</ul>
</div>
</div>
@@ -121,7 +127,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
<div class="swiper">
<div class="swiper-wrapper">{% for project in projects %}
<div class="swiper-slide site" data-url="{{ project.html_url }}">
<img class="site-img" src="{{ project.avatar_url }}" />
<img class="site-img" src="{{ project.avatar_url }}" alt="{{ project.name }} Icon" />
<div class="site-body">
<div class="site-detail" style="width: 100%;">
<h2 class="site-name" style="text-align: left;">{{ project.name }}</h2>
@@ -222,7 +228,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
<div class="container text-center">
<div class="row">
<div class="col">
<p>Verify me with this <a href="pgp" target="_blank">long lifetime Public Key</a> or this <a href="gitpgp" target="_blank">short term one for Github commits</a></p>
<p>Verify me with this <a href="pgp" target="_blank">PGP Public Key</a></p>
</div>
</div>
<div class="row">
@@ -286,7 +292,260 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
<div class="d-none d-print-none d-sm-none d-md-block d-lg-block d-xl-block d-xxl-block clock" style="padding: 1em;background: #10101039;border-top-right-radius: 10px;"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 20 20" fill="none" class="fs-2">
<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>
</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="
display: block;
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
width: 50px;
height: 50px;
border: none;
background: none;
cursor: pointer;
padding: 0;
transition: transform 0.5s ease;
transform: translateX(200%); /* start hidden off-screen *
">
<img src="/assets/img/external/spotify.png" alt="Spotify" style="
width: 100%;
height: 100%;
border-radius: 50%;
"></img>
</button>
<div id="spotify-widget" style="
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
align-items: center;
background: #121212;
color: white;
padding: 10px 15px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
font-family: sans-serif;
max-width: 300px;
z-index: 9999;
transition: transform 0.3s ease, opacity 0.3s ease;
opacity: 0.9;
transform: translateX(120%); /* start hidden off-screen */
">
<img id="spotify-album-art" src="" alt="Album Art" style="
width: 56px;
height: 56px;
border-radius: 6px;
margin-right: 12px;
flex-shrink: 0;
">
<div style="flex: 1; overflow: hidden;">
<div id="spotify-song" style="
font-weight: bold;
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"></div>
<div id="spotify-artist" style="
font-size: 0.85rem;
color: #ccc;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"></div>
<div id="spotify-album" style="
font-size: 0.75rem;
color: #888;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
"></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>
<script>
const widget = document.getElementById('spotify-widget');
const toggleBtn = document.getElementById('spotify-toggle');
function isMobile() {
return window.innerWidth <= 768;
}
function updateVisibility() {
if(isMobile()){
widget.style.transform = 'translateX(120%)'; // hidden off-screen
toggleBtn.style.transform = 'translateX(0)'; // visible
} else {
widget.style.transform = 'translateX(0)'; // visible
toggleBtn.style.transform = 'translateX(200%)'; // hidden off-screen
}
}
// Toggle widget slide in/out on mobile
toggleBtn.addEventListener('click', (e) => {
widget.style.transform = 'translateX(0)'; // slide in
toggleBtn.style.transform = 'translateX(200%)'; // hide button
e.stopPropagation();
});
// Close widget when clicking outside
document.addEventListener('click', (e) => {
if(isMobile()){
if(!widget.contains(e.target) && e.target !== toggleBtn){
widget.style.transform = 'translateX(120%)'; // slide out
toggleBtn.style.transform = 'translateX(0)'; // show button
}
}
});
// Prevent clicks inside widget from closing it
widget.addEventListener('click', (e) => {
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 ---
async function updateSpotifyWidget() {
try {
const res = await fetch('/api/v1/playing');
if (!res.ok) return;
const data = await res.json();
// Check if data contains an error or message indicating nothing is playing
if (data.error || data.message) {
// If existing data
if (document.getElementById('spotify-song').textContent) {
return;
}
// Alternate text when nothing is playing
document.getElementById('spotify-album-art').src = '/assets/img/external/spotify.png';
document.getElementById('spotify-song').textContent = 'Not Playing';
document.getElementById('spotify-artist').textContent = '';
document.getElementById('spotify-album').textContent = '';
document.getElementById('spotify-progress').style.width = '0%';
clearInterval(progressInterval);
progressInterval = null;
currentProgress = 0;
currentTrackId = null;
return;
}
const track = data.spotify;
var firstLoad = false;
// Check if this is the first time loading data
if (!document.getElementById('spotify-song').textContent) {
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-song').textContent = track.song_name;
document.getElementById('spotify-artist').textContent = track.artist;
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) {
updateVisibility();
}
} catch (err) {
console.error('Failed to fetch Spotify data', err);
}
}
// 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
updateSpotifyWidget();
window.addEventListener('resize', updateVisibility);
setInterval(updateSpotifyWidget, 15000);
</script>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>

6
templates/now.ascii Normal file
View File

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

View File

@@ -54,7 +54,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -54,7 +54,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -54,7 +54,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -53,7 +53,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

167
templates/now/25_07_21.html Normal file
View File

@@ -0,0 +1,167 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en-au" style="background: black;height: auto;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>What's up at the moment | Nathan.Woodburn/</title>
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/now/25_07_21">
<meta property="og:url" content="https://nathan.woodburn.au/now/25_07_21">
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:description" content="G'day,
Find out what I've been up to in the last little bit">
<meta name="twitter:title" content="What's up at the moment | Nathan.Woodburn/">
<meta property="og:title" content="What's up at the moment | Nathan.Woodburn/">
<meta name="description" content="G'day,
Find out what I've been up to in the last little bit">
<meta name="twitter:description" content="G'day,
Find out what I've been up to in the last little bit">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/font-awesome.min.css">
<link rel="stylesheet" href="/assets/fonts/ionicons.min.css">
<link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body class="text-center" style="background: linear-gradient(rgba(0,0,0,0.80), rgba(0,0,0,0.80)), url(&quot;/assets/img/bg/background.webp&quot;) center / cover no-repeat;">
<nav class="navbar navbar-expand-md fixed-top navbar-light" id="mainNav" style="background: var(--bs-navbar-hover-color);">
<div class="container-fluid"><a class="navbar-brand" href="/#">
<div style="padding-right: 1em;display: inline-flex;">
<div class="slider"><span>/</span></div><span class="brand">Nathan.Woodburn</span>
</div>
</a><button data-bs-toggle="collapse" class="navbar-toggler navbar-toggler-right" data-bs-target="#navbarResponsive" type="button" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation" value="Menu"><i class="fa fa-bars"></i></button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>
</div>
</div>
</nav>{{handshake_scripts | safe}}
<div style="height: 10em;"></div>
<div class="profile-container" style="margin-bottom: 2em;"><img class="profile background" src="/assets/img/profile.jpg" style="border-radius: 50%;"><img class="profile foreground" src="/assets/img/pfront.webp"></div>
<h1 class="nathanwoodburn" style="margin-bottom: 0px;">Nathan.Woodburn/</h1>
<h3 style="margin-bottom: 0px;">WHat's Happening Now</h3>
<h6>{{DATE}}</h6>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">Uni Semester 2</h1>
<p>I'm back at uni this week. I'm doing 2 courses this semester:&nbsp;Software Engineering,&nbsp;Operating Systems Implementation, Network Security. I'm hoping to finish my degree in semester 1 of 2026.</p>
</div>
</section>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">FireWallet Updates</h1>
<p>I've updated FireWallet, a wallet for Handshake (a decentralized alternative DNS root zone). I've updated the transactions page to display the tx action clearer.<br>There is a new auction page with better UI and dynamic reloading. The auctions now also show mempool bids.</p>
</div>
</section>
<section class="text-center content-section" id="contact" style="padding-top: 0px;padding-bottom: 3em;">
<div class="container">
<div class="row">
<div class="col-lg-8 d-none d-print-block d-sm-block d-md-block d-lg-block d-xl-block d-xxl-block mx-auto">
<div class="social-div">
<ul class="list-unstyled social-list">
<li class="social-link"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
</svg></a></li>
<li class="social-link"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg></a></li>
<li class="social-link"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon"></i></a></li>
<li class="social-link discord"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon">
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
</svg></a></li>
</ul>
</div>
<div class="social-div">
<ul class="list-unstyled social-list">
<li class="social-link mastodon"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
</svg></a></li>
<li class="social-link youtube"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
</svg></a></li>
<li class="social-link signal"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon">
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
</svg></a></li>
</ul>
</div>
</div>
<div class="col-lg-8 d-block d-print-none d-sm-none d-md-none d-lg-none d-xl-none d-xxl-none mx-auto">
<div class="social-div">
<ul class="list-unstyled social-list-sml">
<li class="social-link-sml"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon-sml">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
</svg></a></li>
<li class="social-link-sml"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon-sml">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg></a></li>
<li class="social-link-sml"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon-sml"></i></a></li>
<li class="discord social-link-sml"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon-sml">
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
</svg></a></li>
</ul>
</div>
<div class="social-div">
<ul class="list-unstyled social-list-sml">
<li class="mastodon social-link-sml"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon-sml">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
</svg></a></li>
<li class="youtube social-link-sml"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon-sml">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
</svg></a></li>
<li class="signal social-link-sml"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon-sml">
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
</svg></a></li>
</ul>
</div>
</div>
</div>
</div>
</section>
<footer style="background: #110033;">
<div class="container text-center">
<div class="row">
<div class="col">
<p class="d-none d-print-inline-block d-sm-inline-block d-md-inline-block d-lg-inline-block d-xl-inline-block d-xxl-inline-block">Want to look at some past Now pages?<br>Check out <a href="/old">/old</a></p>
</div>
</div>
<div class="row">
<div class="col">
<p class="d-none d-print-inline-block d-sm-inline-block d-md-inline-block d-lg-inline-block d-xl-inline-block d-xxl-inline-block">This site is also available on<br><a href="https://learn.namebase.io/" target="_blank">Handshake</a>&nbsp;at <a href="https://nathan.woodburn">https://nathan.woodburn/</a></p>
<p class="copyright">Copyright ©&nbsp;Nathan.Woodburn/ 2025</p>
</div>
</div>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/hacker.min.js"></script>
</body>
</html>

179
templates/now/25_08_15.html Normal file
View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en-au" style="background: black;height: auto;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>What's up at the moment | Nathan.Woodburn/</title>
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/now/25_08_15">
<meta property="og:url" content="https://nathan.woodburn.au/now/25_08_15">
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:description" content="G'day,
Find out what I've been up to in the last little bit">
<meta name="twitter:title" content="What's up at the moment | Nathan.Woodburn/">
<meta property="og:title" content="What's up at the moment | Nathan.Woodburn/">
<meta name="description" content="G'day,
Find out what I've been up to in the last little bit">
<meta name="twitter:description" content="G'day,
Find out what I've been up to in the last little bit">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/font-awesome.min.css">
<link rel="stylesheet" href="/assets/fonts/ionicons.min.css">
<link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body class="text-center" style="background: linear-gradient(rgba(0,0,0,0.80), rgba(0,0,0,0.80)), url(&quot;/assets/img/bg/background.webp&quot;) center / cover no-repeat;">
<nav class="navbar navbar-expand-md fixed-top navbar-light" id="mainNav" style="background: var(--bs-navbar-hover-color);">
<div class="container-fluid"><a class="navbar-brand" href="/#">
<div style="padding-right: 1em;display: inline-flex;">
<div class="slider"><span>/</span></div><span class="brand">Nathan.Woodburn</span>
</div>
</a><button data-bs-toggle="collapse" class="navbar-toggler navbar-toggler-right" data-bs-target="#navbarResponsive" type="button" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation" value="Menu"><i class="fa fa-bars"></i></button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>
</div>
</div>
</nav>{{handshake_scripts | safe}}
<div style="height: 10em;"></div>
<div class="profile-container" style="margin-bottom: 2em;"><img class="profile background" src="/assets/img/profile.jpg" style="border-radius: 50%;"><img class="profile foreground" src="/assets/img/pfront.webp"></div>
<h1 class="nathanwoodburn" style="margin-bottom: 0px;">Nathan.Woodburn/</h1>
<h3 style="margin-bottom: 0px;">WHat's Happening Now</h3>
<h6>{{DATE}}</h6>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">HNSDoH Updates</h1>
<p>Lately HNSDoH, my distributed DNS resolver service, has been crashing or having timeouts. In order to mitigate this, I've updated HNSDoH to only route non-ICANN domains to HSD (Handshake domain backend server). This will stop HSD from being swamped with regular traffic and allow it to focus only on resolving Handshake domains. I have also added caching to help with repeat requests.</p>
</div>
</section>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">HNSAU Redesign</h1>
<p>I've updated HNSAU to have an entire new UI thanks to the input from Erwin and Tim. Have a look at <a href="https://hns.au" target="_blank">https://hns.au</a></p>
</div>
</section>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">FireAlerts</h1>
<p>I've recently released an alerts system for your Handshake domains. It will send you a reminder before your domain expires. Have a look at <a href="https://alerts.firewallet.au" target="_blank">https://alerts.firewallet.au</a></p>
</div>
</section>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">Support</h1>
<p>I am currently running low on funding. So if you use any of my projects and would like to support me please send me a message or visit the link below<br><a class="btn btn-primary" role="button" target="_blank" href="/donate">Donate</a></p>
</div>
</section>
<section class="text-center content-section" id="contact" style="padding-top: 0px;padding-bottom: 3em;">
<div class="container">
<div class="row">
<div class="col-lg-8 d-none d-print-block d-sm-block d-md-block d-lg-block d-xl-block d-xxl-block mx-auto">
<div class="social-div">
<ul class="list-unstyled social-list">
<li class="social-link"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
</svg></a></li>
<li class="social-link"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg></a></li>
<li class="social-link"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon"></i></a></li>
<li class="social-link discord"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon">
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
</svg></a></li>
</ul>
</div>
<div class="social-div">
<ul class="list-unstyled social-list">
<li class="social-link mastodon"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
</svg></a></li>
<li class="social-link youtube"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
</svg></a></li>
<li class="social-link signal"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon">
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
</svg></a></li>
</ul>
</div>
</div>
<div class="col-lg-8 d-block d-print-none d-sm-none d-md-none d-lg-none d-xl-none d-xxl-none mx-auto">
<div class="social-div">
<ul class="list-unstyled social-list-sml">
<li class="social-link-sml"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon-sml">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
</svg></a></li>
<li class="social-link-sml"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon-sml">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg></a></li>
<li class="social-link-sml"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon-sml"></i></a></li>
<li class="discord social-link-sml"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon-sml">
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
</svg></a></li>
</ul>
</div>
<div class="social-div">
<ul class="list-unstyled social-list-sml">
<li class="mastodon social-link-sml"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon-sml">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
</svg></a></li>
<li class="youtube social-link-sml"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon-sml">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
</svg></a></li>
<li class="signal social-link-sml"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon-sml">
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
</svg></a></li>
</ul>
</div>
</div>
</div>
</div>
</section>
<footer style="background: #110033;">
<div class="container text-center">
<div class="row">
<div class="col">
<p class="d-none d-print-inline-block d-sm-inline-block d-md-inline-block d-lg-inline-block d-xl-inline-block d-xxl-inline-block">Want to look at some past Now pages?<br>Check out <a href="/old">/old</a></p>
</div>
</div>
<div class="row">
<div class="col">
<p class="d-none d-print-inline-block d-sm-inline-block d-md-inline-block d-lg-inline-block d-xl-inline-block d-xxl-inline-block">This site is also available on<br><a href="https://learn.namebase.io/" target="_blank">Handshake</a>&nbsp;at <a href="https://nathan.woodburn">https://nathan.woodburn/</a></p>
<p class="copyright">Copyright ©&nbsp;Nathan.Woodburn/ 2025</p>
</div>
</div>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/hacker.min.js"></script>
</body>
</html>

173
templates/now/25_10_23.html Normal file
View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en-au" style="background: black;height: auto;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>What's up at the moment | Nathan.Woodburn/</title>
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/now/25_10_23">
<meta property="og:url" content="https://nathan.woodburn.au/now/25_10_23">
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:description" content="G'day,
Find out what I've been up to in the last little bit">
<meta name="twitter:title" content="What's up at the moment | Nathan.Woodburn/">
<meta property="og:title" content="What's up at the moment | Nathan.Woodburn/">
<meta name="description" content="G'day,
Find out what I've been up to in the last little bit">
<meta name="twitter:description" content="G'day,
Find out what I've been up to in the last little bit">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/font-awesome.min.css">
<link rel="stylesheet" href="/assets/fonts/ionicons.min.css">
<link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body class="text-center" style="background: linear-gradient(rgba(0,0,0,0.80), rgba(0,0,0,0.80)), url(&quot;/assets/img/bg/background.webp&quot;) center / cover no-repeat;">
<nav class="navbar navbar-expand-md fixed-top navbar-light" id="mainNav" style="background: var(--bs-navbar-hover-color);">
<div class="container-fluid"><a class="navbar-brand" href="/#">
<div style="padding-right: 1em;display: inline-flex;">
<div class="slider"><span>/</span></div><span class="brand">Nathan.Woodburn</span>
</div>
</a><button data-bs-toggle="collapse" class="navbar-toggler navbar-toggler-right" data-bs-target="#navbarResponsive" type="button" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation" value="Menu"><i class="fa fa-bars"></i></button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>
</div>
</div>
</nav>{{handshake_scripts | safe}}
<div style="height: 10em;"></div>
<div class="profile-container" style="margin-bottom: 2em;"><img class="profile background" src="/assets/img/profile.jpg" style="border-radius: 50%;"><img class="profile foreground" src="/assets/img/pfront.webp"></div>
<h1 class="nathanwoodburn" style="margin-bottom: 0px;">Nathan.Woodburn/</h1>
<h3 style="margin-bottom: 0px;">WHat's Happening Now</h3>
<h6>{{DATE}}</h6>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">Uni Updates</h1>
<p>I'm finishing up uni for the year with exams in the next few weeks. I should be finishing my degree in the first semester of next year. So I'm hoping to find some work for next year to start earning again.</p>
</div>
</section>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">Software Updates</h1>
<p>I haven't done any major updates to my projects lately. I've cleaned up my main website code base to be easier to manage. Other than that I've done a few bug fixes for shaker-bot (a discord verification bot), FireWallet and HNS-Login (a domain authentication service).</p>
</div>
</section>
<section style="margin-bottom: 50px;max-width: 95%;margin-right: auto;margin-left: auto;">
<div style="max-width: 700px;margin: auto;">
<h1 style="margin-bottom: 0px;">5 Years of Github Usage</h1>
<p>This month marks 5 years since my first git commit pushed to Github. In 5 years, I've made over 4,000 commits, 200 repositories, 60 issues, 40 PRs. Of those 40 PRs, I've contributed code to 10 open source projects.</p>
</div>
</section>
<section class="text-center content-section" id="contact" style="padding-top: 0px;padding-bottom: 3em;">
<div class="container">
<div class="row">
<div class="col-lg-8 d-none d-print-block d-sm-block d-md-block d-lg-block d-xl-block d-xxl-block mx-auto">
<div class="social-div">
<ul class="list-unstyled social-list">
<li class="social-link"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
</svg></a></li>
<li class="social-link"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg></a></li>
<li class="social-link"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon"></i></a></li>
<li class="social-link discord"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon">
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
</svg></a></li>
</ul>
</div>
<div class="social-div">
<ul class="list-unstyled social-list">
<li class="social-link mastodon"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
</svg></a></li>
<li class="social-link youtube"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
</svg></a></li>
<li class="social-link signal"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon">
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
</svg></a></li>
</ul>
</div>
</div>
<div class="col-lg-8 d-block d-print-none d-sm-none d-md-none d-lg-none d-xl-none d-xxl-none mx-auto">
<div class="social-div">
<ul class="list-unstyled social-list-sml">
<li class="social-link-sml"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon-sml">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
</svg></a></li>
<li class="social-link-sml"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon-sml">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg></a></li>
<li class="social-link-sml"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon-sml"></i></a></li>
<li class="discord social-link-sml"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon-sml">
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
</svg></a></li>
</ul>
</div>
<div class="social-div">
<ul class="list-unstyled social-list-sml">
<li class="mastodon social-link-sml"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon-sml">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
</svg></a></li>
<li class="youtube social-link-sml"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon-sml">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
</svg></a></li>
<li class="signal social-link-sml"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon-sml">
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
</svg></a></li>
</ul>
</div>
</div>
</div>
</div>
</section>
<footer style="background: #110033;">
<div class="container text-center">
<div class="row">
<div class="col">
<p class="d-none d-print-inline-block d-sm-inline-block d-md-inline-block d-lg-inline-block d-xl-inline-block d-xxl-inline-block">Want to look at some past Now pages?<br>Check out <a href="/old">/old</a></p>
</div>
</div>
<div class="row">
<div class="col">
<p class="d-none d-print-inline-block d-sm-inline-block d-md-inline-block d-lg-inline-block d-xl-inline-block d-xxl-inline-block">This site is also available on<br><a href="https://learn.namebase.io/" target="_blank">Handshake</a>&nbsp;at <a href="https://nathan.woodburn">https://nathan.woodburn/</a></p>
<p class="copyright">Copyright ©&nbsp;Nathan.Woodburn/ 2025</p>
</div>
</div>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/hacker.min.js"></script>
</body>
</html>

View File

@@ -54,7 +54,9 @@ Find out what I've been up to in the last week">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -54,7 +54,9 @@ Find out what I've been up to in the last little bit">
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

9
templates/projects.ascii Normal file
View File

@@ -0,0 +1,9 @@
{{header}}
───────────────────────────────────────────────
 RECENT PROJECTS 
─────────────────
{{projects}}
Look at more projects on my Git: https://git.woodburn.au

View File

@@ -49,7 +49,9 @@
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>

View File

@@ -35,46 +35,94 @@
<link rel="stylesheet" href="/assets/css/resume.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script><link rel="stylesheet" href="/assets/css/resume-print.css" media="print">
</head>
<body style="width: 90%;margin-left: 5%;margin-right: 5%;font-family: 'Noto Sans', sans-serif;">
<div class="d-none d-lg-inline d-xl-inline d-xxl-inline">
<div class="profile-container" style="margin-top: 5em;margin-bottom: 5em;">
<div style="background-color: var(--bs-primary);height: 170px;width: 170px;margin-top: -10px;pointer-events: none;z-index: 1;position: absolute;border-radius: 50%;"></div><img class="profile foreground hideprint" src="/assets/img/nathanwoodburn.jpeg" alt="">
</div>
<div class="title" style="text-align: right;background: var(--bs-primary);">
<h1>Nathan Woodburn</h1>
<p><a href="https://github.com/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;display: inline;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg></a>&nbsp;<a href="https://linkedin.com/in/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;display: inline;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-linkedin">
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854V1.146zm4.943 12.248V6.169H2.542v7.225h2.401m-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248-.822 0-1.359.54-1.359 1.248 0 .694.521 1.248 1.327 1.248h.016zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016a5.54 5.54 0 0 1 .016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225h2.4"></path>
</svg></a>&nbsp;|&nbsp;<a href="mailto:contact@nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;display: inline;" target="_blank">contact@nathan.woodburn.au</a>&nbsp;|&nbsp;<a href="https://nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;display: inline;" target="_blank">https://nathan.woodburn.au</a></p>
</div>
</div>
<div class="d-lg-none d-xl-none d-xxl-none">
<div class="profile-container" style="margin-top: 5em;margin-bottom: 10px;"><img class="profilesml foregroundsml" src="/assets/img/nathanwoodburn.jpeg" style="width: 170px;border: 10px solid var(--bs-primary) ;" alt=""></div>
<div style="text-align: center;margin-bottom: 25px;">
<h1 style="margin-bottom: 0px;">Nathan Woodburn</h1>
<div class="r-small"><a class="print_text" href="mailto:contact@nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;" target="_blank">contact@nathan.woodburn.au</a><span>&nbsp;|&nbsp;</span><a class="print_text" href="https://nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;" target="_blank">https://nathan.woodburn.au</a></div>
<body style="font-family: 'Noto Sans', sans-serif;">
<div class="container-fluid h-100">
<div class="row h-100 resume-row">
<div class="col-md-4 resume-column resume-column-left">
<div class="row row-cols-1 row-fill">
<div class="col">
<div class="text-center"><img class="profile-side" src="/assets/img/nathanwoodburn.jpeg" alt=""></div>
<h1 class="l-heading1">Contact</h1>
<hr class="hr-l">
<div class="r-small"><a class="print_text" href="tel:+61493129562" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-telephone-fill">
<path fill-rule="evenodd" d="M1.885.511a1.745 1.745 0 0 1 2.61.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z"></path>
</svg>&nbsp;+61 493 129 562</a></div>
<div class="r-small"><a class="print_text" href="mailto:nathan@woodburn.au" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-envelope-fill">
<path d="M.05 3.555A2 2 0 0 1 2 2h12a2 2 0 0 1 1.95 1.555L8 8.414.05 3.555ZM0 4.697v7.104l5.803-3.558zM6.761 8.83l-6.57 4.027A2 2 0 0 0 2 14h12a2 2 0 0 0 1.808-1.144l-6.57-4.027L8 9.586l-1.239-.757Zm3.436-.586L16 11.801V4.697l-5.803 3.546Z"></path>
</svg>&nbsp;nathan@woodburn.au</a></div>
<div class="r-small"><span class="print_text"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-pin-angle-fill">
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a5.927 5.927 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707-.195-.195.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a5.922 5.922 0 0 1 1.013.16l3.134-3.133a2.772 2.772 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146z"></path>
</svg>&nbsp;Canberra, ACT</span></div>
<div class="r-small"><a class="print_text" href="https://nathan.woodburn.au" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-link-45deg">
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"></path>
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"></path>
</svg>&nbsp;nathan.woodburn.au</a></div>
<div class="r-small"><a class="print_text" href="https://github.com/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg>&nbsp;@nathanwoodburn</a><span>&nbsp;|&nbsp;</span><a class="print_text" href="https://linkedin.com/in/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-linkedin">
</svg>&nbsp;@nathanwoodburn</a></div>
<div class="r-small"><a class="print_text" href="https://linkedin.com/in/nathanwoodburn" style="color: rgb(255,255,255);text-decoration: none;" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-linkedin">
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854V1.146zm4.943 12.248V6.169H2.542v7.225h2.401m-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248-.822 0-1.359.54-1.359 1.248 0 .694.521 1.248 1.327 1.248h.016zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016a5.54 5.54 0 0 1 .016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225h2.4"></path>
</svg>&nbsp;@nathanwoodburn</a></div>
<div>
<div style="text-align: center;margin-bottom: 25px;"></div>
</div>
</div>
<div style="max-width: 2000px;margin: auto;">
<div style="margin-bottom: 50px;">
<h1 class="r-heading3" style="font-size: 25px;">Summary</h1>
<p class="r-body">Cybersecurity-focused computing student with hands-on experience in DNS, Linux system administration, server infrastructure, and decentralized technologies. Skilled in providing technical support, resolving complex domain-related issues, and engaging in open-source blockchain communities. Experienced speaker and contributor at Handshake-related conferences. Passionate about building secure, resilient, and privacy-respecting systems from the ground up.</p>
</div>
<div class="row row-cols-1 row-cols-lg-2 row-cols-xl-2 row-cols-xxl-2">
<div class="col">
<div class="col side-column">
<h1 class="l-heading1">Education</h1>
<hr class="hr-l">
<div class="noprintbreak">
<h5 class="r-heading2">Bachelor of Computing</h5>
<h6 class="r-heading3">Australian National University<br>2022 - Present</h6>
</div>
<div class="noprintbreak">
<h5 class="r-heading2">Discovering Engineering</h5>
<h6 class="r-heading3">Australian National University<br>Years 11 - 12</h6>
<p class="l-body">Completed enrichment program in engineering disciplines, CAD modeling, and design thinking</p>
</div>
<div class="noprintbreak">
<h5 class="r-heading2">Home Educated</h5>
<h6 class="r-heading3">Self-Directed Learning</h6>
<p class="l-body">Developed passion for technology through independent exploration of programming, system administration, and server management.</p>
</div>
</div>
<div class="col side-column">
<h1 class="l-heading1">Skills</h1>
<hr class="hr-l">
<div class="noprintbreak">
<ul class="r-body">
<li>Python 3</li>
<li>Git</li>
<li>Docker Containerization</li>
<li>DNS</li>
<li>Linux administration</li>
<li>Technical troubleshooting</li>
</ul>
</div>
</div>
</div>
</div>
<div class="col resume-column resume-column-right">
<div style="margin: 3em;">
<h1 class="title" style="margin-bottom: 0px;">Nathan Woodburn</h1>
<h1 class="subtitle r-heading3" style="font-size: 25px;color: var(--bs-gray);">{% if support %}Technical Support Specialist{% else %}Linux Systems Administrator{% endif %}</h1>
<hr class="title-hr">
</div>
<div class="l-summary">
<h1 class="r-heading1">Profile</h1>
<hr class="hr-l-primary">
<p class="r-body">{% if support %}Technical Support Specialist with expertise in Linux, DNS, and network troubleshooting. Experienced in resolving critical domain and network issues, supporting end-users, and collaborating with engineering teams to ensure stable and secure systems. Skilled in Python automation to streamline repetitive tasks and improve operational efficiency.{% else %}System Administrator specializing in Linux, Docker, and server deployments. Experienced in managing Proxmox, networks, and CI/CD pipelines. Implementing Python automations to optimize system operations. Ability to deploy and maintain server environments, self-hosted services, and web applications while ensuring reliability, scalability, and security.{% endif %}</p>
</div>
<div class="row g-0 row-cols-1">
<div class="col">
<h1 class="r-heading1">Experience</h1>
<h4 class="r-heading2">Technical Support Specialist</h4>
<h6 class="r-heading3">Namebase - Remote |&nbsp;Oct 2022 - JUN 2025</h6>
<hr class="hr-l-primary">
<div class="noprintbreak">
<h4 class="l-heading2 float-right">Oct 2022 - Jun 2025</h4>
<h4 class="l-heading2">Technical Support Specialist</h4>
<h6 class="l-heading3">Namebase - Remote</h6>
<ul class="r-body">
<li>Provided technical support for users, focusing on domain setup, configuration, and troubleshooting.</li>
<li>Worked with engineering teams to report bugs and suggest product improvements.</li>
@@ -82,186 +130,46 @@
<li>Gained hands-on experience with recursive and authoritative DNS, DNSSEC, and decentralized naming.</li>
<li>Engaged with the community through social platforms and represented Namebase at conferences.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Small Business Owner</h4>
<h6 class="r-heading3">Nathan 3D Printing Service |&nbsp;Feb 2020 - Dec 2023</h6>
<h4 class="l-heading2 float-right">Feb 2020 - Dec 2023</h4>
<h4 class="l-heading2">Small Business Owner</h4>
<h6 class="l-heading3">Nathan 3D Printing Service</h6>
<ul class="r-body">
<li>Operated a custom 3D printing and CAD design business independently.</li>
<li>Handled client communication, design iteration, and order fulfillment.</li>
<li>Built end-to-end project management and technical design skills.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Audio Production&nbsp;Volunteer</h4>
<h6 class="r-heading3">1WAY FM |&nbsp;Feb 2021 - Dec 2021</h6>
<ul class="r-body">
<li>Recorded, edited, and produced audio content for community radio broadcasts.</li>
<li>Supported the production team in day-to-day technical operations.</li>
<li>Gained practical skills in audio engineering and collaborative media work.</li>
</ul>
<hr>
</div>
</div>
<div class="col edu-main">
<div class="noprintbreak">
<h1 class="r-heading1">Education</h1>
<h4 class="r-heading2">Bachelor of Computing</h4>
<h6 class="r-heading3">Australian National University |&nbsp;2022 - Present</h6>
<ul class="r-body">
<li>Currently pursuing a Bachelor of Computing with a specialization in cybersecurity.</li>
<li>Gaining hands-on experience in network security, cryptography, and secure software development.</li>
<li>Building a strong foundation in computer science principles, programming, and system architecture.</li>
<li>Collaborating on group projects and labs to apply theoretical knowledge to real-world challenges.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Discovering Engineering</h4>
<h6 class="r-heading3">Australian National University |&nbsp;YearS 11 &amp; 12</h6>
<ul class="r-body">
<li>Completed an enrichment program introducing core engineering disciplines and technical concepts.</li>
<li>Explored CAD modeling, design thinking, and practical problem-solving through workshops and case studies.</li>
<li>Gained early exposure to engineering tools and technical communication, laying the groundwork for later technical studies.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Home Educated</h4>
<h6 class="r-heading3">Self-Directed Learning</h6>
<ul class="r-body">
<li>Cultivated time management, self-discipline, and critical thinking skills crucial for success in tech and cybersecurity.</li>
<li>Developed a strong passion for technology, programming, and system administration through independent exploration.</li>
<li>Built custom applications, managed servers, and solved technical challenges in a flexible learning environment.</li>
</ul>
<hr>
</div>
</div>
</div>
<div class="spacer"></div>
<div class="col">
<div class="noprintbreak">
<h1 class="r-heading1">Projects</h1>
<h4 class="r-heading2">FireWallet</h4>
<h6 class="r-heading3">Python, Handshake, Plugin Architecture</h6>
<ul class="r-body">
<li>Developed a modular Python-based Handshake wallet with plugin support for extensibility.</li>
<li>Presented at HandyCon 2024 and 2025, showcasing usability improvements and HNS site resolution.</li>
</ul>
<hr>
</div>
<hr class="hr-l-primary">
<div class="noprintbreak">
<h4 class="r-heading2">Server Lab</h4>
<h6 class="r-heading3">Proxmox, Networking, Linux, DNS</h6>
<h4 class="l-heading2">Server Lab</h4>
<h6 class="l-heading3">Proxmox, Networking, Linux, DNS</h6>
<ul class="r-body">
<li>Maintain a personal physical server running Proxmox hypervisor.</li>
<li>Host multiple virtual machines across three VLANs with isolated firewalls for enhanced security.</li>
<li>Provide DNS and recursive resolver hosting services for external users.</li>
<li>Host a suite of self-hosted services such as Gitea, Authentik, Vaultwarden and Nextcloud.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">HNSDoH</h4>
<h6 class="r-heading3">DNS, Handshake, DoH, Distributed Systems, Linux</h6>
<h4 class="l-heading2">Personal Website</h4>
<h6 class="l-heading3">Python 3, Flask, Docker, CI/CD</h6>
<ul class="r-body">
<li>Manage a distributed Handshake DoH resolver network spanning six independent nodes.</li>
<li>Administer four nodes and collaborate with two external operators on updates, patches, and troubleshooting.</li>
<li>Ensure uptime and resiliency across geographically distributed infrastructure.</li>
<li>Designed modular web application architecture with Flask blueprints and reusable templates.</li>
<li>Managed containerized deployment using Docker on a dedicated server, ensuring consistency and scalability.</li>
<li>Implemented CI/CD pipelines for automated testing, building, and deployment from Git.</li>
<li>Integrated dynamic content and interactive features while maintaining secure and optimized server operations.</li>
</ul>
<hr>
</div>
</div>
<div class="spacer"></div>
<div>
<div class="noprintbreak">
<h1 class="r-heading1">Skills</h1>
<h4 class="r-heading2">Programming &amp; Development</h4>
<ul class="r-body">
<li><strong>Python 3</strong>: Proficient in building web services and automation tools; experienced with libraries such as Flask, requests, and asyncio.</li>
<li><strong>C &amp; Java</strong>: Applied in university coursework and labs for systems programming, algorithms, and object-oriented design.</li>
<li><strong>C#</strong>: Experienced in building Windows applications, including debugging and testing since 2016.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Networking &amp; Security</h4>
<ul class="r-body">
<li><strong>DNS &amp; DNSSEC</strong>: Skilled in managing DNS zones, records, and DNSSEC; experienced with both authoritative and recursive resolvers.</li>
<li><strong>Linux System Administration</strong>: Manage cloud and physical servers, using the command line for scripting, security, and package management.</li>
<li><strong>Server Infrastructure</strong>: Operate a dedicated server running Proxmox; manage virtual machines across VLANs with separate firewalls to enhance isolation and security.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Technical Support &amp; Communication</h4>
<ul class="r-body">
<li><strong>Technical Support</strong>: Deliver front-line technical assistance, troubleshoot software/platform issues, and communicate clearly with users.</li>
<li><strong>Community Engagement</strong>: Active contributor and presenter within the Handshake and blockchain communities.</li>
<li><strong>Tools</strong>: Git, Docker, NGINX, SSH, Bash scripting.</li>
</ul>
<hr>
</div>
</div>
<div class="spacer"></div>
<div>
<h1 class="r-heading1">Conferences</h1>
<div class="noprintbreak">
<h4 class="r-heading2">Presenter HandyCon 2025</h4>
<h6 class="r-heading3">Online | March 2025</h6>
<ul class="r-body">
<li><strong>Firewallet Updates &amp; How to Resolve HNS Sites</strong> Presented new features and usability improvements in FireWallet, including user-friendly Handshake resolution methods.</li>
<li><strong>Building the Future of Handshake: Advancing Wallets &amp; Ecosystem Development</strong> (co-presented with Rithvik Vibhu) Discussed strategies for wallet development, improving developer tooling, and enhancing the decentralized web experience on Handshake.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Judge &amp; Speaker Onchain Names &amp; Identity Hackathon</h4>
<h6 class="r-heading3">Vietnam | April 2024</h6>
<ul class="r-body">
<li>Invited judge for blockchain-focused hackathon entries using Handshake and decentralized identity tools.</li>
<li>Delivered a talk comparing Handshake DNS with traditional DNS systems, highlighting benefits of decentralized roots for security and censorship resistance.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Presenter HandyCon 2024</h4>
<h6 class="r-heading3">Online | March 2024</h6>
<ul class="r-body">
<li><strong>FireWallet</strong> Showcased a modular Handshake wallet written in Python, designed with plugin support to enable extensibility and developer customization.</li>
</ul>
<hr>
</div>
<div class="noprintbreak">
<h4 class="r-heading2">Presenter HandyCon 2023</h4>
<h6 class="r-heading3">Online | March 2023</h6>
<ul class="r-body">
<li>Presented a technical walkthrough on launching websites with Handshake domains.</li>
<li>Covered HTTPS setup using DANE to eliminate reliance on traditional certificate authorities.</li>
</ul>
<hr>
</div>
</div>
<div class="spacer"></div>
</div>
<footer class="text-center bg-dark d-print-none" style="width: 99vw;margin-left: -5vw;padding: 0px;">
<div class="container text-white py-4 py-lg-5" style="width: auto;max-width: 100%;">
<ul class="list-inline">
<li class="list-inline-item me-4"><a href="https://www.facebook.com/nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-facebook text-light">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"></path>
</svg></a></li>
<li class="list-inline-item me-4"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter text-light">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15"></path>
</svg></a></li>
<li class="list-inline-item me-4"><a href="https://github.com/nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github text-light">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
</svg></a></li>
</ul>
<p class="text-muted mb-0" style="color: rgb(255,255,255) !important;">Copyright © Nathan.Woodburn/ 2025</p>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>

View File

@@ -66,6 +66,12 @@
<url>
<loc>https://nathan.woodburn.au/now/25_06_19</loc>
</url>
<url>
<loc>https://nathan.woodburn.au/now/25_08_15</loc>
</url>
<url>
<loc>https://nathan.woodburn.au/now/25_10_23</loc>
</url>
<url>
<loc>https://nathan.woodburn.au/now/old</loc>
</url>
@@ -75,6 +81,9 @@
<url>
<loc>https://nathan.woodburn.au/donate</loc>
</url>
<url>
<loc>https://nathan.woodburn.au/hosting</loc>
</url>
<url>
<loc>https://nathan.woodburn.au/</loc>
</url>
@@ -87,4 +96,7 @@
<url>
<loc>https://nathan.woodburn.au/resume</loc>
</url>
<url>
<loc>https://nathan.woodburn.au/tools</loc>
</url>
</urlset>

20
templates/tools.ascii Normal file
View File

@@ -0,0 +1,20 @@
{{header}}
───────────────────────────────────────────────
 Tools 
────────────
Here are some of the tools I use regularly — most of them are open source! 🛠️
{% for type, tools_in_type in tools | groupby('type') %}
{{type}}
{% for tool in tools_in_type %}
{{tool.name}}
{{tool.description}}
Website: {{tool.url}}
{% if tool.demo %}Demo: {{tool.demo}}{% endif %}
{% endfor %}
───────────────────────────────────────────────
{% endfor %}

215
templates/tools.html Normal file
View File

@@ -0,0 +1,215 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en-au">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Tools | Nathan.Woodburn/</title>
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/tools">
<meta property="og:url" content="https://nathan.woodburn.au/tools">
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:title" content="Nathan.Woodburn/">
<meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website">
<meta name="twitter:title" content="Nathan.Woodburn/">
<meta property="og:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta name="description" content="Check out some tools I use">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/font-awesome.min.css">
<link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="stylesheet" href="/assets/css/tools.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="77">
<nav class="navbar navbar-expand-md fixed-top navbar-light" id="mainNav" style="background: var(--bs-navbar-hover-color);">
<div class="container-fluid"><a class="navbar-brand" href="/#">
<div style="padding-right: 1em;display: inline-flex;">
<div class="slider"><span>/</span></div><span class="brand">Nathan.Woodburn</span>
</div>
</a><button data-bs-toggle="collapse" class="navbar-toggler navbar-toggler-right" data-bs-target="#navbarResponsive" type="button" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation" value="Menu"><i class="fa fa-bars"></i></button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/projects">Projects</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>
</div>
</div>
</nav>
<header class="masthead" style="background: url(&quot;/assets/img/bg/projects.webp&quot;) bottom / cover no-repeat;height: auto;padding-top: 20px;">
<div style="margin-top: 150px;margin-bottom: 100px;">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<h1 class="brand-heading">Tools</h1>
<p>Here is a list of applications, tools and services I use regularly.</p>
</div>
</div>
</div>
</div>
</header>
<section class="text-center content-section" id="tools" style="padding-bottom: 100px;">
<div class="container">{% for type, tools_in_type in tools | groupby('type') %}
<h2 class="mt-4 mb-3 sticky-top bg-primary py-2 section-header" id="{{type}}">{{ type }}</h2>
<div class="row">
{% for tool in tools_in_type %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 shadow-sm transition-all" style="transition: transform 0.2s, box-shadow 0.2s;">
<div class="card-body d-flex flex-column">
<h4 class="card-title">{{tool.name}}</h4>
<p class="card-text">{{ tool.description }}</p>
<div class="btn-group gap-3 mt-auto" role="group">{% if tool.demo %}<button class="btn btn-primary"
type="button" data-bs-target="#modal-{{tool.name}}" data-bs-toggle="modal"
style="transition: transform 0.2s, background-color 0.2s;">View Demo</button>{% endif %}<a
class="btn btn-primary" role="button" href="{{tool.url}}" target="_blank"
style="transition: transform 0.2s, background-color 0.2s;">{{tool.name}} Website</a></div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Modals for this type -->
{% for tool in tools_in_type %}
{% if tool.demo %}
<div id="modal-{{tool.name}}" class="modal fade" role="dialog" tabindex="-1" style="z-index: 1055;"
data-demo-url="{{ tool.demo | e }}">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{tool.name}}</h4><button class="btn-close" type="button" aria-label="Close"
data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" data-demo-loaded="false"></div>
</div>
<div class="modal-footer"><button class="btn btn-light" type="button" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% endfor %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const navbar = document.getElementById('mainNav');
const headers = document.querySelectorAll('.section-header');
if (navbar) {
const navbarHeight = navbar.offsetHeight;
headers.forEach(header => {
header.style.top = navbarHeight + 'px';
header.style.zIndex = '100';
header.style.scrollMarginTop = navbarHeight + 'px';
});
// Handle hash navigation on page load
if (window.location.hash) {
setTimeout(() => {
const target = document.querySelector(window.location.hash);
if (target) {
window.scrollTo({
top: target.offsetTop - navbarHeight,
behavior: 'smooth'
});
}
}, 0);
}
}
// Load demo in modal
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('show.bs.modal', () => {
const body = modal.querySelector('.modal-body');
if (body.dataset.demoLoaded === 'false') {
const demoUrl = modal.dataset.demoUrl;
const iframeId = 'iframe-' + modal.id;
// Add a div on top of all content to show loading message
const loadingDiv = document.createElement('div');
loadingDiv.style.position = 'absolute';
loadingDiv.style.top = '0';
loadingDiv.style.left = '0';
loadingDiv.style.width = '100%';
loadingDiv.style.height = '100%';
loadingDiv.style.backgroundColor = 'rgb(0, 0, 0)';
loadingDiv.style.display = 'flex';
loadingDiv.style.justifyContent = 'center';
loadingDiv.style.alignItems = 'center';
loadingDiv.style.zIndex = '10';
const loadingMsg = document.createElement('p');
loadingMsg.className = 'text-center';
loadingMsg.textContent = 'Loading demo...';
loadingDiv.appendChild(loadingMsg);
body.style.position = 'relative';
body.appendChild(loadingDiv);
// Create iframe
const iframe = document.createElement('iframe');
iframe.src = demoUrl + '/iframe';
iframe.id = iframeId;
iframe.style.width = '100%';
iframe.style.height = '400px'; // temporary height
iframe.style.border = '0';
iframe.setAttribute('scrolling', 'no');
iframe.setAttribute('allowfullscreen', 'true');
body.appendChild(iframe);
body.dataset.demoLoaded = 'true';
// Listen for bodySize message from asciinema iframe
const origin = new URL(demoUrl).origin;
function onMessage(event) {
if (event.origin !== origin || event.source !== iframe.contentWindow) return;
if (event.data.type === 'bodySize' && event.data.payload.height) {
iframe.style.height = event.data.payload.height + 'px';
// Remove loading message
body.removeChild(loadingDiv);
// Optional: limit modal max height
modal.querySelector('.modal-dialog').style.maxHeight = '90vh';
}
}
window.addEventListener('message', onMessage, false);
}
});
});
});
</script></div>
</section>
<footer>
<div class="container text-center">
<p class="copyright">Copyright ©&nbsp;Nathan.Woodburn/ 2025</p>
</div>
</footer>{{handshake_scripts | safe}}
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/hacker.min.js"></script>
</body>
</html>

3
tests/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Tests
These tests use hurl. Note that the SOL tests are slow as they create transactions

28
tests/api.hurl Normal file
View File

@@ -0,0 +1,28 @@
GET http://127.0.0.1:5000/api/v1/
HTTP 200
GET http://127.0.0.1:5000/api/v1/help
HTTP 200
GET http://127.0.0.1:5000/api/v1/ip
HTTP 200
[Asserts]
jsonpath "$.ip" matches "^(127|172).(0|17).0.1$"
GET http://127.0.0.1:5000/api/v1/time
HTTP 200
GET http://127.0.0.1:5000/api/v1/timezone
HTTP 200
[Asserts]
jsonpath "$.timezone" >= 10
jsonpath "$.timezone" <= 12
GET http://127.0.0.1:5000/api/v1/message
HTTP 200
GET http://127.0.0.1:5000/api/v1/project
HTTP 200
GET http://127.0.0.1:5000/api/v1/tools
HTTP 200
[Asserts]
jsonpath "$.tools" count > 5
GET http://127.0.0.1:5000/api/v1/playing
HTTP 200

9
tests/blog.hurl Normal file
View File

@@ -0,0 +1,9 @@
GET http://127.0.0.1:5000/blog/
HTTP 200
GET http://127.0.0.1:5000/blog/Fingertip_on_Linux_Mint
HTTP 200
GET http://127.0.0.1:5000/blog/Fingertip_on_Linux_Mint.md
HTTP 200

41
tests/legacy_api.hurl Normal file
View File

@@ -0,0 +1,41 @@
GET http://127.0.0.1:5000/api/help
HTTP 301
[Asserts]
header "Location" == "/api/v1/help"
GET http://127.0.0.1:5000/api/ip
HTTP 301
[Asserts]
header "Location" == "/api/v1/ip"
GET http://127.0.0.1:5000/api/message
HTTP 301
[Asserts]
header "Location" == "/api/v1/message"
GET http://127.0.0.1:5000/api/project
HTTP 301
[Asserts]
header "Location" == "/api/v1/project"
GET http://127.0.0.1:5000/api/donate
HTTP 301
[Asserts]
header "Location" == "/api/v1/donate"
GET http://127.0.0.1:5000/api/time
HTTP 301
[Asserts]
header "Location" == "/api/v1/time"
GET http://127.0.0.1:5000/api/timezone
HTTP 301
[Asserts]
header "Location" == "/api/v1/timezone"
GET http://127.0.0.1:5000/api/version
HTTP 301
[Asserts]
header "Location" == "/api/v1/version"

24
tests/now.hurl Normal file
View File

@@ -0,0 +1,24 @@
GET http://127.0.0.1:5000/now/
HTTP 200
GET http://127.0.0.1:5000/now/old
HTTP 200
GET http://127.0.0.1:5000/now/24_02_18
HTTP 200
GET http://127.0.0.1:5000/now/24_02_18
HTTP 200
GET http://127.0.0.1:5000/now/now.json
HTTP 200
GET http://127.0.0.1:5000/now/now.xml
HTTP 200
GET http://127.0.0.1:5000/now/now.rss
HTTP 200
GET http://127.0.0.1:5000/now/rss.xml
HTTP 200

14
tests/sol.slow_hurl Normal file
View File

@@ -0,0 +1,14 @@
POST http://127.0.0.1:5000/api/v1/donate/1
{"account": "1111111111111111111111111111111B"}
POST http://127.0.0.1:5000/api/v1/donate/0.01
{"account": "1111111111111111111111111111111C"}
POST http://127.0.0.1:5000/api/v1/donate/0.1
{"account": "1111111111111111111111111111111D"}
POST http://127.0.0.1:5000/api/v1/donate/0.02
{"account": "1111111111111111111111111111111E"}
POST http://127.0.0.1:5000/api/v1/donate/{amount}
{"account": "11111111111111111111111111111112"}

3
tests/test.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
hurl --test *.hurl

11
tests/well-known.hurl Normal file
View File

@@ -0,0 +1,11 @@
GET http://127.0.0.1:5000/.well-known/xrp-ledger.toml
HTTP 200
GET http://127.0.0.1:5000/.well-known/nostr.json?name=hurl
HTTP 200
[Asserts]
jsonpath "$.names.hurl" == "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82"
GET http://127.0.0.1:5000/.well-known/wallets/BTC
HTTP 200

279
tools.py Normal file
View File

@@ -0,0 +1,279 @@
from flask import Request, render_template, jsonify, make_response
import os
from functools import lru_cache as cache
import datetime
from typing import Optional, Dict, Union, Tuple
import re
from dateutil.parser import parse
import json
# HTTP status codes
HTTP_OK = 200
HTTP_BAD_REQUEST = 400
HTTP_NOT_FOUND = 404
CRAWLERS = [
"Googlebot",
"Bingbot",
"Chrome-Lighthouse",
"Slurp",
"DuckDuckBot",
"Baiduspider",
"YandexBot",
"Sogou",
"Exabot",
"facebot",
"ia_archiver",
"Twitterbot"
]
CLI_AGENTS = [
"curl",
"hurl",
"xh",
"Posting",
"HTTPie"
]
def getClientIP(request: Request) -> str:
"""
Get the client's IP address from the request.
Args:
request (Request): The Flask request object
Returns:
str: The client's IP address
"""
x_forwarded_for = request.headers.get("X-Forwarded-For")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0]
else:
ip = request.remote_addr
if ip is None:
ip = "unknown"
return ip
@cache
def getGitCommit() -> str:
"""
Get the current git commit hash.
Returns:
str: The current git commit hash or a failure message
"""
# 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"
def isCLI(request: Request) -> bool:
"""
Check if the request is from curl or hurl.
Args:
request (Request): The Flask request object
Returns:
bool: True if the request is from curl or hurl, False otherwise
"""
if request.headers and request.headers.get("User-Agent"):
user_agent = request.headers.get("User-Agent", "")
return any(agent in user_agent for agent in CLI_AGENTS)
return False
def isCrawler(request: Request) -> bool:
"""
Check if the request is from a web crawler (e.g., Googlebot, Bingbot).
Args:
request (Request): The Flask request object
Returns:
bool: True if the request is from a web crawler, False otherwise
"""
if request.headers and request.headers.get("User-Agent"):
user_agent = request.headers.get("User-Agent", "")
return any(crawler in user_agent for crawler in CRAWLERS)
return False
@cache
def isDev(host: str) -> bool:
"""
Check if the host indicates a development environment.
Args:
host (str): The host string from the request
Returns:
bool: True if in development environment, False otherwise
"""
if (
host == "localhost:5000"
or host == "127.0.0.1:5000"
or os.getenv("DEV") == "true"
or host == "test.nathan.woodburn.au"
):
return True
return False
@cache
def getHandshakeScript(host: str) -> str:
"""
Get the handshake script HTML snippet.
Args:
domain (str): The domain to use in the handshake script
Returns:
str: The handshake script HTML snippet
"""
if isDev(host):
return ""
return '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
@cache
def getAddress(coin: str) -> str:
"""
Get the wallet address for a cryptocurrency.
Args:
coin (str): The cryptocurrency code
Returns:
str: The wallet address or empty string if not found
"""
address = ""
wallet_path = f".well-known/wallets/{coin.upper()}"
if os.path.isfile(wallet_path):
with open(wallet_path) as file:
address = file.read()
return address
@cache
def getFilePath(name: str, path: str) -> Optional[str]:
"""
Find a file in a directory tree.
Args:
name (str): The filename to find
path (str): The root directory to search
Returns:
Optional[str]: The full path to the file or None if not found
"""
for root, dirs, files in os.walk(path):
if name in files:
return os.path.join(root, name)
return None
def json_response(request: Request, message: Union[str, Dict] = "404 Not Found", code: int = 404):
"""
Create a JSON response with standard formatting.
Args:
request (Request): The Flask request object
message (Union[str, Dict]): The response message or data
code (int): The HTTP status code
Returns:
Tuple[Dict, int]: The JSON response and HTTP status code
"""
if isinstance(message, dict):
# Add status and ip to dict
message["status"] = code
message["ip"] = getClientIP(request)
return jsonify(message), code
return jsonify({
"status": code,
"message": message,
"ip": getClientIP(request),
}), code
def error_response(
request: Request,
message: str = "404 Not Found",
code: int = 404,
force_json: bool = False
) -> Union[Tuple[Dict, int], object]:
"""
Create an error response in JSON or HTML format.
Args:
request (Request): The Flask request object
message (str): The error message
code (int): The HTTP status code
force_json (bool): Whether to force JSON response regardless of client
Returns:
Union[Tuple[Dict, int], object]: The JSON or HTML response
"""
if force_json or isCLI(request):
return json_response(request, message, code)
# Check if <error code>.html exists in templates
template_name = f"{code}.html" if os.path.isfile(
f"templates/{code}.html") else "404.html"
response = make_response(render_template(
template_name, code=code, message=message), code)
# Add message to response headers
response.headers["X-Error-Message"] = message
return response
def parse_date(date_groups: list[str]) -> str | None:
"""
Parse a list of date components into YYYY-MM-DD format.
Uses dateutil.parser for robust parsing.
Works for:
- DD Month YYYY
- Month DD, YYYY
- YYYY-MM-DD
- YYYYMMDD
- Month YYYY (defaults day to 1)
- Handles ordinal suffixes (st, nd, rd, th)
"""
try:
# Join date groups into a single string
date_str = " ".join(date_groups).strip()
# Remove ordinal suffixes
date_str = re.sub(r'(\d+)(st|nd|rd|th)', r'\1',
date_str, flags=re.IGNORECASE)
# Parse with dateutil, default day=1 if missing
dt = parse(date_str, default=datetime.datetime(1900, 1, 1))
# If year is missing, parse will fallback to 1900 → reject
if dt.year == 1900:
return None
return dt.strftime("%Y-%m-%d")
except (ValueError, TypeError):
return None
def get_tools_data():
with open("data/tools.json", "r") as f:
return json.load(f)

718
uv.lock generated Normal file
View File

@@ -0,0 +1,718 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "ansi2html"
version = "1.9.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/d5/e3546dcd5e4a9566f4ed8708df5853e83ca627461a5b048a861c6f8e7a26/ansi2html-1.9.2.tar.gz", hash = "sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5", size = 44300, upload-time = "2024-06-22T17:33:23.964Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/71/aee71b836e9ee2741d5694b80d74bfc7c8cd5dbdf7a9f3035fcf80d792b1/ansi2html-1.9.2-py3-none-any.whl", hash = "sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5", size = 17614, upload-time = "2024-06-22T17:33:21.852Z" },
]
[[package]]
name = "anyio"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "cachetools"
version = "6.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "click"
version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
]
[[package]]
name = "cloudflare"
version = "4.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/48/e481c0a9b9010a5c41b5ca78ff9fbe00dc8a9a4d39da5af610a4ec49c7f7/cloudflare-4.3.1.tar.gz", hash = "sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258", size = 1933187, upload-time = "2025-06-16T21:43:18.716Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/8f/c6c543565efd3144da4304efa5917aac06b6416a8663a6defe0e9b2b7569/cloudflare-4.3.1-py3-none-any.whl", hash = "sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6", size = 4406465, upload-time = "2025-06-16T21:43:17.3Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "construct"
version = "2.10.68"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/a4a032e94bcfdff481f2e6fecd472794d9da09f474a2185ed33b2c7cad64/construct-2.10.68.tar.gz", hash = "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45", size = 57856, upload-time = "2022-02-21T23:09:15.1Z" }
[[package]]
name = "construct-typing"
version = "0.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "construct" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/13/c609e60a687252813aa4b69f989f42754ccd5e217717216fc852eefedfd7/construct-typing-0.6.2.tar.gz", hash = "sha256:948e998cfc003681dc34f2d071c3a688cf35b805cbe107febbc488ef967ccba1", size = 22029, upload-time = "2023-08-03T07:31:06.205Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/0b/ab3ce2b27dd74b6a6703065bd304ea8211ff4de3b1c304446ed95234177b/construct_typing-0.6.2-py3-none-any.whl", hash = "sha256:ebea6989ac622d0c4eb457092cef0c7bfbcfa110bd018670fea7064d0bc09e47", size = 23298, upload-time = "2023-08-03T07:31:04.545Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "flask"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
]
[[package]]
name = "flask-cors"
version = "6.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flask" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/37/bcfa6c7d5eec777c4c7cf45ce6b27631cebe5230caf88d85eadd63edd37a/flask_cors-6.0.1.tar.gz", hash = "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db", size = 13463, upload-time = "2025-06-11T01:32:08.518Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/f8/01bf35a3afd734345528f98d0353f2a978a476528ad4d7e78b70c4d149dd/flask_cors-6.0.1-py3-none-any.whl", hash = "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", size = 13244, upload-time = "2025-06-11T01:32:07.352Z" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "jsonalias"
version = "0.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/45/ee7e17002cb7f3264f755ff6a1a72c55d1830e07808d643167d2a2277c4f/jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769", size = 1095, upload-time = "2022-10-28T22:57:56.224Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/ed/05aebce69f78c104feff2ffcdd5a6f9d668a208aba3a8bf56e3750809fd8/jsonalias-0.1.1-py3-none-any.whl", hash = "sha256:a56d2888e6397812c606156504e861e8ec00e188005af149f003c787db3d3f18", size = 1312, upload-time = "2022-10-28T22:57:54.763Z" },
]
[[package]]
name = "markdown"
version = "3.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "nathanwoodburn-github-io"
version = "1.1.0"
source = { virtual = "." }
dependencies = [
{ name = "ansi2html" },
{ name = "beautifulsoup4" },
{ name = "cachetools" },
{ name = "cloudflare" },
{ name = "flask" },
{ name = "flask-cors" },
{ name = "gunicorn" },
{ name = "markdown" },
{ name = "pillow" },
{ name = "pydantic" },
{ name = "pygments" },
{ name = "python-dateutil" },
{ name = "python-dotenv" },
{ name = "qrcode" },
{ name = "requests" },
{ name = "solana" },
{ name = "solders" },
]
[package.metadata]
requires-dist = [
{ name = "ansi2html", specifier = ">=1.9.2" },
{ name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "cachetools", specifier = ">=6.2.1" },
{ name = "cloudflare", specifier = ">=4.3.1" },
{ name = "flask", specifier = ">=3.1.2" },
{ name = "flask-cors", specifier = ">=6.0.1" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "markdown", specifier = ">=3.9" },
{ name = "pillow", specifier = ">=12.0.0" },
{ name = "pydantic", specifier = ">=2.12.3" },
{ name = "pygments", specifier = ">=2.19.2" },
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "qrcode", specifier = ">=8.2" },
{ name = "requests", specifier = ">=2.32.5" },
{ name = "solana", specifier = ">=0.36.9" },
{ name = "solders", specifier = ">=0.26.0" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pillow"
version = "12.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
{ url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
{ url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
{ url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
{ url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
{ url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
{ url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
{ url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
{ url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
{ url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
{ url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
{ url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
{ url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
{ url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
{ url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
{ url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
{ url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
{ url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
{ url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
{ url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
{ url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
{ url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
{ url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
{ url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
{ url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
{ url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
{ url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
{ url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
{ url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
{ url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
{ url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
{ url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
{ url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
{ url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
{ url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
{ url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
{ url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
{ url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
]
[[package]]
name = "pydantic"
version = "2.12.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "qrcode"
version = "8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "solana"
version = "0.36.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "construct-typing" },
{ name = "httpx" },
{ name = "solders" },
{ name = "typing-extensions" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/e0/ce762b6763e3a0f8a5ccecbf695d65ef54b6f874ad5f58ce5cdcaba224f1/solana-0.36.9.tar.gz", hash = "sha256:f702f6177337c67a982909ef54ef3abce5e795b8cd93edb045bedfa4d13c20c5", size = 52722, upload-time = "2025-08-09T16:23:25.307Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/11/d5e5d02200ca85b615da39078806b377156b67b2093c8bc08a1b9c293070/solana-0.36.9-py3-none-any.whl", hash = "sha256:e05824f91f95abe5a687914976e8bc78986386156f2106108c696db998c3c542", size = 62882, upload-time = "2025-08-09T16:23:24.149Z" },
]
[[package]]
name = "solders"
version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jsonalias" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/96/23ad2e43e2676b78834064fe051e3db3ce1899336ecd4797f92fcd06113a/solders-0.26.0.tar.gz", hash = "sha256:057533892d6fa432c1ce1e2f5e3428802964666c10b57d3d1bcaab86295f046c", size = 181123, upload-time = "2025-02-18T19:23:57.734Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/ce/58bbb4d2c696e770cdd37e5f6dc2891ef7610c0c085bf400f9c42dcff1ad/solders-0.26.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c1a0ef5daa1a05934af5fb6e7e32eab7c42cede406c80067fee006f461ffc4a", size = 24344472, upload-time = "2025-02-18T19:23:30.273Z" },
{ url = "https://files.pythonhosted.org/packages/5a/35/221cec0e5900c2202833e7e9110c3405a2d96ed25e110b247f88b8782e29/solders-0.26.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b964efbd7c0b38aef3bf4293ea5938517ae649b9a23e7cd147d889931775aab", size = 6674734, upload-time = "2025-02-18T19:23:35.15Z" },
{ url = "https://files.pythonhosted.org/packages/41/33/d17b7dbc92672351d59fc65cdb93b8924fc682deba09f6d96f25440187ae/solders-0.26.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e6a769c5298b887b7588edb171d93709a89302aef75913fe893d11c653739d", size = 13472961, upload-time = "2025-02-18T19:23:38.582Z" },
{ url = "https://files.pythonhosted.org/packages/bb/e7/533367d815ab000587ccc37d89e154132f63347f02dcaaac5df72bd851de/solders-0.26.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b3cc55b971ec6ed1b4466fa7e7e09eee9baba492b8cd9e3204e3e1a0c5a0c4aa", size = 6886198, upload-time = "2025-02-18T19:23:41.453Z" },
{ url = "https://files.pythonhosted.org/packages/52/e0/ab41ab3df5fdf3b0e55613be93a43c2fe58b15a6ea8ceca26d3fba02e3c6/solders-0.26.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3e3973074c17265921c70246a17bcf80972c5b96a3e1ed7f5049101f11865092", size = 7319170, upload-time = "2025-02-18T19:23:43.758Z" },
{ url = "https://files.pythonhosted.org/packages/7d/34/5174ce592607e0ac020aff203217f2f113a55eec49af3db12945fea42d89/solders-0.26.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:59b52419452602f697e659199a25acacda8365971c376ef3c0687aecdd929e07", size = 7134977, upload-time = "2025-02-18T19:23:46.157Z" },
{ url = "https://files.pythonhosted.org/packages/ba/5e/822faabda0d473c29bdf59fe8869a411fd436af8ca6f5d6e89f7513f682f/solders-0.26.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5946ec3f2a340afa9ce5c2b8ab628ae1dea2ad2235551b1297cafdd7e3e5c51a", size = 6984222, upload-time = "2025-02-18T19:23:49.429Z" },
{ url = "https://files.pythonhosted.org/packages/23/e8/dc992f677762ea2de44b7768120d95887ef39fab10d6f29fb53e6a9882c1/solders-0.26.0-cp37-abi3-win_amd64.whl", hash = "sha256:5466616610170aab08c627ae01724e425bcf90085bc574da682e9f3bd954900b", size = 5480492, upload-time = "2025-02-18T19:23:53.285Z" },
]
[[package]]
name = "soupsieve"
version = "2.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "websockets"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
]
[[package]]
name = "werkzeug"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
]