Compare commits
61 Commits
665921d046
...
feat/bluep
| Author | SHA1 | Date | |
|---|---|---|---|
|
72b8dae35e
|
|||
|
323ace5775
|
|||
|
c2803e372a
|
|||
|
2a9e704f29
|
|||
|
0c490625a9
|
|||
|
b9753617ad
|
|||
|
b87d19c5d9
|
|||
|
67e8b4cf7e
|
|||
|
bfc6652f29
|
|||
|
38372c0cff
|
|||
|
dd64313006
|
|||
|
9e20a6171a
|
|||
|
da347fd860
|
|||
|
776b7de753
|
|||
|
7b2b3659bb
|
|||
|
872373dffd
|
|||
|
8d832372cd
|
|||
|
03dae87272
|
|||
|
4c654fcb78
|
|||
|
c9542e4af7
|
|||
|
e184375897
|
|||
|
844f1b52e2
|
|||
|
19c51c3665
|
|||
|
85ebd460ed
|
|||
|
50879b4f0e
|
|||
|
6c09923281
|
|||
|
332c408b89
|
|||
|
4ae6f7bb99
|
|||
|
d4d6b47225
|
|||
|
9809fe0695
|
|||
|
3522389422
|
|||
|
2979d3c4de
|
|||
|
a8b2c02164
|
|||
|
372ba908b8
|
|||
|
1145b9205c
|
|||
|
a71c5b6663
|
|||
|
724e800201
|
|||
|
abcaa9283d
|
|||
|
e175f68d25
|
|||
|
80b6a9bf46
|
|||
|
b089b8c0a8
|
|||
|
8f774ba8f0
|
|||
|
f4f5f47ee7
|
|||
|
16f17a9486
|
|||
|
72483674f6
|
|||
|
b69c7f381b
|
|||
|
d7d4dbed8b
|
|||
|
2437b19836
|
|||
|
abd23e0eb8
|
|||
|
57a4b977ec
|
|||
|
7f591e2724
|
|||
|
3d5c16f9cb
|
|||
|
fdb5f84c92
|
|||
|
eaf363ee27
|
|||
|
0ea9db3473
|
|||
|
8d6acca5e9
|
|||
|
bfc1f0839a
|
|||
|
258061c64d
|
|||
|
399ac5f0da
|
|||
|
74362de02a
|
|||
|
9f7b93b8a1
|
18
.gitea/workflows/check.yml
Normal file
18
.gitea/workflows/check.yml
Normal 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,5 +1,6 @@
|
|||||||
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
|
FROM --platform=$BUILDPLATFORM python:3.13-alpine AS builder
|
||||||
|
|
||||||
|
RUN apk add curl
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt /app
|
COPY requirements.txt /app
|
||||||
@@ -14,4 +15,4 @@ COPY . /app
|
|||||||
ENTRYPOINT ["python3"]
|
ENTRYPOINT ["python3"]
|
||||||
CMD ["main.py"]
|
CMD ["main.py"]
|
||||||
|
|
||||||
FROM builder AS dev-envs
|
FROM builder AS dev-envs
|
||||||
|
|||||||
BIN
NathanWoodburn.bsdesign
Normal file
BIN
NathanWoodburn.bsdesign
Normal file
Binary file not shown.
36
blueprints/acme.py
Normal file
36
blueprints/acme.py
Normal 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)
|
||||||
@@ -1,247 +1,324 @@
|
|||||||
from flask import Blueprint, request, jsonify, make_response
|
from flask import Blueprint, request, jsonify
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
import requests
|
import requests
|
||||||
|
import re
|
||||||
from mail import sendEmail
|
from mail import sendEmail
|
||||||
from sol import create_transaction
|
from tools import getClientIP, getGitCommit, json_response, parse_date, get_tools_data
|
||||||
from tools import getClientIP
|
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
|
||||||
|
|
||||||
api_bp = Blueprint('api', __name__)
|
app = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||||
|
# Register solana blueprint
|
||||||
|
app.register_blueprint(sol.app)
|
||||||
|
|
||||||
ncReq = requests.get(
|
# Load configuration
|
||||||
|
NC_CONFIG = requests.get(
|
||||||
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
|
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
|
||||||
)
|
).json()
|
||||||
ncConfig = ncReq.json()
|
|
||||||
|
|
||||||
if 'time-zone' not in ncConfig:
|
if 'time-zone' not in NC_CONFIG:
|
||||||
ncConfig['time-zone'] = 10
|
NC_CONFIG['time-zone'] = 10
|
||||||
|
|
||||||
|
|
||||||
def getGitCommit():
|
@app.route("/", strict_slashes=False)
|
||||||
# if .git exists, get the latest commit hash
|
@app.route("/help")
|
||||||
if os.path.isdir(".git"):
|
def help():
|
||||||
git_dir = ".git"
|
"""Provide API documentation and help."""
|
||||||
head_ref = ""
|
|
||||||
with open(os.path.join(git_dir, "HEAD")) as file:
|
|
||||||
head_ref = file.read().strip()
|
|
||||||
if head_ref.startswith("ref: "):
|
|
||||||
head_ref = head_ref[5:]
|
|
||||||
with open(os.path.join(git_dir, head_ref)) as file:
|
|
||||||
return file.read().strip()
|
|
||||||
else:
|
|
||||||
return head_ref
|
|
||||||
|
|
||||||
# Check if env SOURCE_COMMIT is set
|
|
||||||
if "SOURCE_COMMIT" in os.environ:
|
|
||||||
return os.environ["SOURCE_COMMIT"]
|
|
||||||
|
|
||||||
return "failed to get version"
|
|
||||||
|
|
||||||
@api_bp.route("/")
|
|
||||||
@api_bp.route("/help")
|
|
||||||
def help_get():
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au",
|
"message": "Welcome to Nathan.Woodburn/ API! This is a personal website. For more information, visit https://nathan.woodburn.au",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"/time": "Get the current time",
|
"/time": "Get the current time",
|
||||||
"/timezone": "Get the current timezone",
|
"/timezone": "Get the current timezone",
|
||||||
"/message": "Get the message from the config",
|
"/message": "Get the message from the config",
|
||||||
"/ip": "Get your IP address",
|
|
||||||
"/project": "Get the current project from git",
|
"/project": "Get the current project from git",
|
||||||
"/version": "Get the current version of the website",
|
"/version": "Get the current version of the website",
|
||||||
|
"/page_date?url=URL&verbose=BOOL": "Get the last modified date of a webpage (verbose is optional, default false)",
|
||||||
|
"/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"
|
"/help": "Get this help message"
|
||||||
},
|
},
|
||||||
"version": getGitCommit()
|
"base_url": "/api/v1",
|
||||||
|
"version": getGitCommit(),
|
||||||
|
"ip": getClientIP(request),
|
||||||
|
"status": HTTP_OK
|
||||||
})
|
})
|
||||||
|
|
||||||
@api_bp.route("/version")
|
@app.route("/status")
|
||||||
def version_get():
|
@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()})
|
return jsonify({"version": getGitCommit()})
|
||||||
|
|
||||||
@api_bp.route("/time")
|
|
||||||
def time_get():
|
@app.route("/time")
|
||||||
timezone_offset = datetime.timedelta(hours=ncConfig["time-zone"])
|
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)
|
timezone = datetime.timezone(offset=timezone_offset)
|
||||||
time = datetime.datetime.now(tz=timezone)
|
current_time = datetime.datetime.now(tz=timezone)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"timestring": time.strftime("%A, %B %d, %Y %I:%M %p"),
|
"timestring": current_time.strftime("%A, %B %d, %Y %I:%M %p"),
|
||||||
"timestamp": time.timestamp(),
|
"timestamp": current_time.timestamp(),
|
||||||
"timezone": ncConfig["time-zone"],
|
"timezone": NC_CONFIG["time-zone"],
|
||||||
"timeISO": time.isoformat()
|
"timeISO": current_time.isoformat(),
|
||||||
|
"ip": getClientIP(request),
|
||||||
|
"status": HTTP_OK
|
||||||
})
|
})
|
||||||
|
|
||||||
@api_bp.route("/timezone")
|
|
||||||
def timezone_get():
|
|
||||||
return jsonify({"timezone": ncConfig["time-zone"]})
|
|
||||||
|
|
||||||
@api_bp.route("/timezone", methods=["POST"])
|
@app.route("/timezone")
|
||||||
def timezone_post():
|
def timezone():
|
||||||
# Refresh config
|
"""Get the current timezone setting."""
|
||||||
global ncConfig
|
return jsonify({
|
||||||
conf = requests.get(
|
"timezone": NC_CONFIG["time-zone"],
|
||||||
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json")
|
"ip": getClientIP(request),
|
||||||
if conf.status_code != 200:
|
"status": HTTP_OK
|
||||||
return jsonify({"message": "Error: Could not get timezone"})
|
})
|
||||||
if not conf.json():
|
|
||||||
return jsonify({"message": "Error: Could not get timezone"})
|
|
||||||
conf = conf.json()
|
|
||||||
if "time-zone" not in conf:
|
|
||||||
return jsonify({"message": "Error: Could not get timezone"})
|
|
||||||
|
|
||||||
ncConfig = conf
|
|
||||||
return jsonify({"message": "Successfully pulled latest timezone", "timezone": ncConfig["time-zone"]})
|
|
||||||
|
|
||||||
@api_bp.route("/message")
|
|
||||||
def message_get():
|
|
||||||
return jsonify({"message": ncConfig["message"]})
|
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/ip")
|
@app.route("/message")
|
||||||
def ip_get():
|
def message():
|
||||||
return jsonify({"ip": getClientIP(request)})
|
"""Get the message from the configuration."""
|
||||||
|
return jsonify({
|
||||||
|
"message": NC_CONFIG["message"],
|
||||||
|
"ip": getClientIP(request),
|
||||||
|
"status": HTTP_OK
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/email", methods=["POST"])
|
@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():
|
def email_post():
|
||||||
|
"""Send an email via the API (requires API key)."""
|
||||||
# Verify json
|
# Verify json
|
||||||
if not request.is_json:
|
if not request.is_json:
|
||||||
return jsonify({
|
return json_response(request, "415 Unsupported Media Type", HTTP_UNSUPPORTED_MEDIA)
|
||||||
"status": 400,
|
|
||||||
"error": "Bad request JSON Data missing"
|
|
||||||
})
|
|
||||||
|
|
||||||
# Check if api key sent
|
# Check if api key sent
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({
|
return json_response(request, "400 Bad Request", HTTP_BAD_REQUEST)
|
||||||
"status": 400,
|
|
||||||
"error": "Bad request JSON Data missing"
|
|
||||||
})
|
|
||||||
|
|
||||||
if "key" not in data:
|
if "key" not in data:
|
||||||
return jsonify({
|
return json_response(request, "400 Bad Request 'key' missing", HTTP_BAD_REQUEST)
|
||||||
"status": 401,
|
|
||||||
"error": "Unauthorized 'key' missing"
|
|
||||||
})
|
|
||||||
|
|
||||||
if data["key"] != os.getenv("EMAIL_KEY"):
|
if data["key"] != os.getenv("EMAIL_KEY"):
|
||||||
return jsonify({
|
return json_response(request, "401 Unauthorized", HTTP_UNAUTHORIZED)
|
||||||
"status": 401,
|
|
||||||
"error": "Unauthorized 'key' invalid"
|
|
||||||
})
|
|
||||||
|
|
||||||
|
# TODO: Add client info to email
|
||||||
return sendEmail(data)
|
return sendEmail(data)
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/project")
|
@app.route("/project")
|
||||||
def project_get():
|
def project():
|
||||||
|
"""Get information about the current git project."""
|
||||||
|
gitinfo = {
|
||||||
|
"website": None,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
git = requests.get(
|
git = requests.get(
|
||||||
"https://git.woodburn.au/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
||||||
headers={"Authorization": os.getenv("git_token")},
|
headers={"Authorization": os.getenv("git_token")},
|
||||||
)
|
)
|
||||||
git = git.json()
|
git = git.json()
|
||||||
git = git[0]
|
git = git[0]
|
||||||
repo_name = git["repo"]["name"]
|
repo_name = git["repo"]["name"]
|
||||||
repo_name = repo_name.lower()
|
repo_name = repo_name.lower()
|
||||||
repo_description = git["repo"]["description"]
|
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:
|
except Exception as e:
|
||||||
repo_name = "nathanwoodburn.github.io"
|
|
||||||
repo_description = "Personal website"
|
|
||||||
git = {
|
|
||||||
"repo": {
|
|
||||||
"html_url": "https://nathan.woodburn.au",
|
|
||||||
"name": "nathanwoodburn.github.io",
|
|
||||||
"description": "Personal website",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
print(f"Error getting git data: {e}")
|
print(f"Error getting git data: {e}")
|
||||||
|
return json_response(request, "500 Internal Server Error", HTTP_SERVER_ERROR)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"repo_name": repo_name,
|
"repo_name": repo_name,
|
||||||
"repo_description": repo_description,
|
"repo_description": repo_description,
|
||||||
"git": git,
|
"repo": gitinfo,
|
||||||
|
"ip": getClientIP(request),
|
||||||
|
"status": HTTP_OK
|
||||||
})
|
})
|
||||||
|
|
||||||
# region Solana Links
|
@app.route("/tools")
|
||||||
SOLANA_HEADERS = {
|
def tools():
|
||||||
"Content-Type": "application/json",
|
"""Get a list of tools used by Nathan Woodburn."""
|
||||||
"X-Action-Version": "2.4.2",
|
|
||||||
"X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/donate", methods=["GET", "OPTIONS"])
|
|
||||||
def sol_donate_get():
|
|
||||||
data = {
|
|
||||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
|
||||||
"label": "Donate to Nathan.Woodburn/",
|
|
||||||
"title": "Donate to Nathan.Woodburn/",
|
|
||||||
"description": "Student, developer, and crypto enthusiast",
|
|
||||||
"links": {
|
|
||||||
"actions": [
|
|
||||||
{"label": "0.01 SOL", "href": "/api/v1/donate/0.01"},
|
|
||||||
{"label": "0.1 SOL", "href": "/api/v1/donate/0.1"},
|
|
||||||
{"label": "1 SOL", "href": "/api/v1/donate/1"},
|
|
||||||
{
|
|
||||||
"href": "/api/v1/donate/{amount}",
|
|
||||||
"label": "Donate",
|
|
||||||
"parameters": [
|
|
||||||
{"name": "amount", "label": "Enter a custom SOL amount"}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
response = make_response(jsonify(data), 200, SOLANA_HEADERS)
|
|
||||||
|
|
||||||
if request.method == "OPTIONS":
|
|
||||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
||||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, OPTIONS"
|
|
||||||
response.headers["Access-Control-Allow-Headers"] = (
|
|
||||||
"Content-Type,Authorization,Content-Encoding,Accept-Encoding,X-Action-Version,X-Blockchain-Ids"
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/donate/<amount>")
|
|
||||||
def sol_donate_amount_get(amount):
|
|
||||||
data = {
|
|
||||||
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
|
|
||||||
"label": f"Donate {amount} SOL to Nathan.Woodburn/",
|
|
||||||
"title": "Donate to Nathan.Woodburn/",
|
|
||||||
"description": f"Donate {amount} SOL to Nathan.Woodburn/",
|
|
||||||
}
|
|
||||||
return jsonify(data), 200, SOLANA_HEADERS
|
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route("/donate/<amount>", methods=["POST"])
|
|
||||||
def sol_donate_post(amount):
|
|
||||||
|
|
||||||
if not request.json:
|
|
||||||
return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS
|
|
||||||
|
|
||||||
|
|
||||||
if "account" not in request.json:
|
|
||||||
return jsonify({"message": "Error: No account provided"}), 400, SOLANA_HEADERS
|
|
||||||
|
|
||||||
sender = request.json["account"]
|
|
||||||
|
|
||||||
# Make sure amount is a number
|
|
||||||
try:
|
try:
|
||||||
amount = float(amount)
|
tools = get_tools_data()
|
||||||
except ValueError:
|
except Exception as e:
|
||||||
amount = 1 # Default to 1 SOL if invalid
|
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)
|
||||||
|
|
||||||
if amount < 0.0001:
|
@app.route("/playing")
|
||||||
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
|
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)
|
||||||
|
|
||||||
transaction = create_transaction(sender, amount)
|
|
||||||
return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS
|
|
||||||
|
|
||||||
# endregion
|
@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)
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
from flask import Blueprint, render_template, request
|
from flask import Blueprint, render_template, request, jsonify
|
||||||
import markdown
|
import markdown
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import re
|
import re
|
||||||
|
from tools import isCLI, getClientIP, getHandshakeScript
|
||||||
|
|
||||||
blog_bp = Blueprint('blog', __name__)
|
app = Blueprint('blog', __name__, url_prefix='/blog')
|
||||||
|
|
||||||
|
|
||||||
def list_blog_page_files():
|
def list_page_files():
|
||||||
blog_pages = os.listdir("data/blog")
|
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
|
# Remove .md extension
|
||||||
blog_pages = [page.removesuffix(".md")
|
blog_pages = [page.removesuffix(".md")
|
||||||
for page in blog_pages if page.endswith(".md")]
|
for page in blog_pages if page.endswith(".md")]
|
||||||
@@ -16,7 +21,7 @@ def list_blog_page_files():
|
|||||||
return blog_pages
|
return blog_pages
|
||||||
|
|
||||||
|
|
||||||
def render_blog_page(date, handshake_scripts=None):
|
def render_page(date, handshake_scripts=None):
|
||||||
# Convert md to html
|
# Convert md to html
|
||||||
if not os.path.exists(f"data/blog/{date}.md"):
|
if not os.path.exists(f"data/blog/{date}.md"):
|
||||||
return render_template("404.html"), 404
|
return render_template("404.html"), 404
|
||||||
@@ -82,9 +87,9 @@ def fix_numbered_lists(html):
|
|||||||
return str(soup)
|
return str(soup)
|
||||||
|
|
||||||
|
|
||||||
def render_blog_home(handshake_scripts=None):
|
def render_home(handshake_scripts: str | None = None):
|
||||||
# Get a list of pages
|
# Get a list of pages
|
||||||
blog_pages = list_blog_page_files()
|
blog_pages = list_page_files()
|
||||||
# Create a html list of pages
|
# Create a html list of pages
|
||||||
blog_pages = [
|
blog_pages = [
|
||||||
f"""<li class="list-group-item">
|
f"""<li class="list-group-item">
|
||||||
@@ -103,32 +108,57 @@ def render_blog_home(handshake_scripts=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@blog_bp.route("/")
|
@app.route("/", strict_slashes=False)
|
||||||
def blog_index_get():
|
def index():
|
||||||
global handshake_scripts
|
if not isCLI(request):
|
||||||
|
return render_home(handshake_scripts=getHandshakeScript(request.host))
|
||||||
|
|
||||||
# If localhost, don't load handshake
|
# Get a list of pages
|
||||||
if (
|
blog_pages = list_page_files()
|
||||||
request.host == "localhost:5000"
|
# Create a html list of pages
|
||||||
or request.host == "127.0.0.1:5000"
|
blog_pages = [
|
||||||
or os.getenv("dev") == "true"
|
{"name": page.replace("_", " "), "url": f"/blog/{page}", "download": f"/blog/{page}.md"} for page in blog_pages
|
||||||
or request.host == "test.nathan.woodburn.au"
|
]
|
||||||
):
|
|
||||||
handshake_scripts = ""
|
|
||||||
|
|
||||||
return render_blog_home(handshake_scripts)
|
# Render the template
|
||||||
|
return jsonify({
|
||||||
|
"status": 200,
|
||||||
|
"message": "Check out my various blog postsa",
|
||||||
|
"ip": getClientIP(request),
|
||||||
|
"blogs": blog_pages
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
@blog_bp.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def blog_path_get(path):
|
def path(path):
|
||||||
global handshake_scripts
|
if not isCLI(request):
|
||||||
# If localhost, don't load handshake
|
return render_page(path, handshake_scripts=getHandshakeScript(request.host))
|
||||||
if (
|
|
||||||
request.host == "localhost:5000"
|
|
||||||
or request.host == "127.0.0.1:5000"
|
|
||||||
or os.getenv("dev") == "true"
|
|
||||||
or request.host == "test.nathan.woodburn.au"
|
|
||||||
):
|
|
||||||
handshake_scripts = ""
|
|
||||||
|
|
||||||
return render_blog_page(path, handshake_scripts)
|
# 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'}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
from flask import Blueprint, render_template, make_response, request, jsonify
|
from flask import Blueprint, render_template, make_response, request, jsonify
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
from tools import getHandshakeScript, error_response, isCLI
|
||||||
|
from curl import get_header, MAX_WIDTH
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import re
|
||||||
|
|
||||||
# Create blueprint
|
# Create blueprint
|
||||||
now_bp = Blueprint('now', __name__)
|
app = Blueprint('now', __name__, url_prefix='/now')
|
||||||
|
|
||||||
def list_now_page_files():
|
|
||||||
|
def list_page_files():
|
||||||
now_pages = os.listdir("templates/now")
|
now_pages = os.listdir("templates/now")
|
||||||
now_pages = [
|
now_pages = [
|
||||||
page for page in now_pages if page != "template.html" and page != "old.html"
|
page for page in now_pages if page != "template.html" and page != "old.html"
|
||||||
@@ -13,90 +18,146 @@ def list_now_page_files():
|
|||||||
now_pages.sort(reverse=True)
|
now_pages.sort(reverse=True)
|
||||||
return now_pages
|
return now_pages
|
||||||
|
|
||||||
def list_now_dates():
|
|
||||||
now_pages = list_now_page_files()
|
def list_dates():
|
||||||
|
now_pages = list_page_files()
|
||||||
now_dates = [page.split(".")[0] for page in now_pages]
|
now_dates = [page.split(".")[0] for page in now_pages]
|
||||||
return now_dates
|
return now_dates
|
||||||
|
|
||||||
def get_latest_now_date(formatted=False):
|
|
||||||
|
def get_latest_date(formatted=False):
|
||||||
if formatted:
|
if formatted:
|
||||||
date=list_now_dates()[0]
|
date = list_dates()[0]
|
||||||
date = datetime.datetime.strptime(date, "%y_%m_%d")
|
date = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||||
date = date.strftime("%A, %B %d, %Y")
|
date = date.strftime("%A, %B %d, %Y")
|
||||||
return date
|
return date
|
||||||
return list_now_dates()[0]
|
return list_dates()[0]
|
||||||
|
|
||||||
def render_latest_now(handshake_scripts=None):
|
|
||||||
now_page = list_now_dates()[0]
|
|
||||||
return render_now_page(now_page,handshake_scripts=handshake_scripts)
|
|
||||||
|
|
||||||
def render_now_page(date,handshake_scripts=None):
|
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 the date is not available, render the latest page
|
||||||
if date is None:
|
if date is None:
|
||||||
return render_latest_now(handshake_scripts=handshake_scripts)
|
return render_latest(handshake_scripts=handshake_scripts)
|
||||||
# Remove .html
|
# Remove .html
|
||||||
date = date.removesuffix(".html")
|
date = date.removesuffix(".html")
|
||||||
|
|
||||||
if date not in list_now_dates():
|
if date not in list_dates():
|
||||||
return render_template("404.html"), 404
|
return error_response(request)
|
||||||
|
|
||||||
|
|
||||||
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
|
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
|
||||||
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
|
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
|
||||||
return render_template(f"now/{date}.html",DATE=date_formatted,handshake_scripts=handshake_scripts)
|
return render_template(f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts)
|
||||||
|
|
||||||
@now_bp.route("/")
|
def render_curl(date=None):
|
||||||
def now_index_get():
|
# If the date is not available, render the latest page
|
||||||
handshake_scripts = ''
|
if date is None:
|
||||||
# If localhost, don't load handshake
|
date = get_latest_date()
|
||||||
if (
|
|
||||||
request.host == "localhost:5000"
|
|
||||||
or request.host == "127.0.0.1:5000"
|
|
||||||
or os.getenv("dev") == "true"
|
|
||||||
or request.host == "test.nathan.woodburn.au"
|
|
||||||
):
|
|
||||||
handshake_scripts = ""
|
|
||||||
else:
|
|
||||||
handshake_scripts = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
|
||||||
|
|
||||||
return render_latest_now(handshake_scripts)
|
# 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"[1m{post['header']}[0m\n\n{post['content']}\n\n"
|
||||||
|
|
||||||
|
return render_template("now.ascii", date=date_formatted, content=response, header=get_header())
|
||||||
|
|
||||||
|
|
||||||
@now_bp.route("/<path:path>")
|
|
||||||
def now_path_get(path):
|
|
||||||
handshake_scripts = ''
|
|
||||||
# If localhost, don't load handshake
|
|
||||||
if (
|
|
||||||
request.host == "localhost:5000"
|
|
||||||
or request.host == "127.0.0.1:5000"
|
|
||||||
or os.getenv("dev") == "true"
|
|
||||||
or request.host == "test.nathan.woodburn.au"
|
|
||||||
):
|
|
||||||
handshake_scripts = ""
|
|
||||||
else:
|
|
||||||
handshake_scripts = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
|
||||||
|
|
||||||
return render_now_page(path, handshake_scripts)
|
@app.route("/", strict_slashes=False)
|
||||||
|
def index():
|
||||||
|
if isCLI(request):
|
||||||
|
return render_curl()
|
||||||
|
return render_latest(handshake_scripts=getHandshakeScript(request.host))
|
||||||
|
|
||||||
|
|
||||||
@now_bp.route("/old")
|
@app.route("/<path:path>")
|
||||||
@now_bp.route("/old/")
|
def path(path):
|
||||||
def now_old_get():
|
if isCLI(request):
|
||||||
handshake_scripts = ''
|
return render_curl(path)
|
||||||
# If localhost, don't load handshake
|
|
||||||
if (
|
return render(path, handshake_scripts=getHandshakeScript(request.host))
|
||||||
request.host == "localhost:5000"
|
|
||||||
or request.host == "127.0.0.1:5000"
|
|
||||||
or os.getenv("dev") == "true"
|
@app.route("/old", strict_slashes=False)
|
||||||
or request.host == "test.nathan.woodburn.au"
|
def old():
|
||||||
):
|
now_dates = list_dates()[1:]
|
||||||
handshake_scripts = ""
|
if isCLI(request):
|
||||||
else:
|
response = ""
|
||||||
handshake_scripts = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
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())
|
||||||
|
|
||||||
|
|
||||||
now_dates = list_now_dates()[1:]
|
|
||||||
html = '<ul class="list-group">'
|
html = '<ul class="list-group">'
|
||||||
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_now_date(True)}</li></a>'
|
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_date(True)}</li></a>'
|
||||||
|
|
||||||
for date in now_dates:
|
for date in now_dates:
|
||||||
link = date
|
link = date
|
||||||
@@ -106,19 +167,19 @@ def now_old_get():
|
|||||||
|
|
||||||
html += "</ul>"
|
html += "</ul>"
|
||||||
return render_template(
|
return render_template(
|
||||||
"now/old.html", handshake_scripts=handshake_scripts, now_pages=html
|
"now/old.html", handshake_scripts=getHandshakeScript(request.host), now_pages=html
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@now_bp.route("/now.rss")
|
@app.route("/now.rss")
|
||||||
@now_bp.route("/now.xml")
|
@app.route("/now.xml")
|
||||||
@now_bp.route("/rss.xml")
|
@app.route("/rss.xml")
|
||||||
def now_rss_get():
|
def rss():
|
||||||
host = "https://" + request.host
|
host = "https://" + request.host
|
||||||
if ":" in request.host:
|
if ":" in request.host:
|
||||||
host = "http://" + request.host
|
host = "http://" + request.host
|
||||||
# Generate RSS feed
|
# Generate RSS feed
|
||||||
now_pages = list_now_page_files()
|
now_pages = list_page_files()
|
||||||
rss = f'<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nathan.Woodburn/</title><link>{host}</link><description>See what I\'ve been up to</description><language>en-us</language><lastBuildDate>{datetime.datetime.now(tz=datetime.timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")}</lastBuildDate><atom:link href="{host}/now.rss" rel="self" type="application/rss+xml" />'
|
rss = f'<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nathan.Woodburn/</title><link>{host}</link><description>See what I\'ve been up to</description><language>en-us</language><lastBuildDate>{datetime.datetime.now(tz=datetime.timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")}</lastBuildDate><atom:link href="{host}/now.rss" rel="self" type="application/rss+xml" />'
|
||||||
for page in now_pages:
|
for page in now_pages:
|
||||||
link = page.strip(".html")
|
link = page.strip(".html")
|
||||||
@@ -129,9 +190,9 @@ def now_rss_get():
|
|||||||
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
|
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
|
||||||
|
|
||||||
|
|
||||||
@now_bp.route("/now.json")
|
@app.route("/now.json")
|
||||||
def now_json_get():
|
def json():
|
||||||
now_pages = list_now_page_files()
|
now_pages = list_page_files()
|
||||||
host = "https://" + request.host
|
host = "https://" + request.host
|
||||||
if ":" in request.host:
|
if ":" in request.host:
|
||||||
host = "http://" + request.host
|
host = "http://" + request.host
|
||||||
|
|||||||
59
blueprints/podcast.py
Normal file
59
blueprints/podcast.py
Normal 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
125
blueprints/sol.py
Normal 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
|
||||||
126
blueprints/spotify.py
Normal file
126
blueprints/spotify.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
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 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 json_response(request, {"error": "Failed to refresh access token"}, 500)
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
response = requests.get(SPOTIFY_CURRENTLY_PLAYING_URL, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 204:
|
||||||
|
return {"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
9
blueprints/template.py
Normal 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)
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
from flask import Blueprint, render_template, make_response, request, jsonify, send_from_directory, redirect
|
from flask import Blueprint, make_response, request, jsonify, send_from_directory, redirect
|
||||||
|
from tools import error_response
|
||||||
import os
|
import os
|
||||||
|
|
||||||
wk_bp = Blueprint('well-known', __name__)
|
app = Blueprint('well-known', __name__, url_prefix='/.well-known')
|
||||||
|
|
||||||
|
|
||||||
@wk_bp.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def wk_index_get(path):
|
def index(path):
|
||||||
return send_from_directory(".well-known", path)
|
return send_from_directory(".well-known", path)
|
||||||
|
|
||||||
|
|
||||||
@wk_bp.route("/wallets/<path:path>")
|
@app.route("/wallets/<path:path>")
|
||||||
def wk_wallet_get(path):
|
def wallets(path):
|
||||||
if path[0] == "." and 'proof' not in path:
|
if path[0] == "." and 'proof' not in path:
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
".well-known/wallets", path, mimetype="application/json"
|
".well-known/wallets", path, mimetype="application/json"
|
||||||
@@ -25,11 +26,11 @@ def wk_wallet_get(path):
|
|||||||
if os.path.isfile(".well-known/wallets/" + path.upper()):
|
if os.path.isfile(".well-known/wallets/" + path.upper()):
|
||||||
return redirect("/.well-known/wallets/" + path.upper(), code=302)
|
return redirect("/.well-known/wallets/" + path.upper(), code=302)
|
||||||
|
|
||||||
return render_template("404.html"), 404
|
return error_response(request)
|
||||||
|
|
||||||
|
|
||||||
@wk_bp.route("/nostr.json")
|
@app.route("/nostr.json")
|
||||||
def wk_nostr_get():
|
def nostr():
|
||||||
# Get name parameter
|
# Get name parameter
|
||||||
name = request.args.get("name")
|
name = request.args.get("name")
|
||||||
if name:
|
if name:
|
||||||
@@ -50,8 +51,8 @@ def wk_nostr_get():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@wk_bp.route("/xrp-ledger.toml")
|
@app.route("/xrp-ledger.toml")
|
||||||
def wk_xrp_get():
|
def xrp():
|
||||||
# Create a response with the xrp-ledger.toml file
|
# Create a response with the xrp-ledger.toml file
|
||||||
with open(".well-known/xrp-ledger.toml") as file:
|
with open(".well-known/xrp-ledger.toml") as file:
|
||||||
toml = file.read()
|
toml = file.read()
|
||||||
|
|||||||
132
curl.py
Normal file
132
curl.py
Normal 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"[1;36m{repo_name}[0m"
|
||||||
|
return f"[1;36m{repo_name}[0m - [1m{repo_description}[0m"
|
||||||
|
|
||||||
|
|
||||||
|
@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"""[1m{project['name']}[0m - {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'}
|
||||||
53
data/blog/Software_I_Use.md
Normal file
53
data/blog/Software_I_Use.md
Normal 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
|
||||||
|
|
||||||
171
data/tools.json
Normal file
171
data/tools.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -14,4 +14,5 @@ solders
|
|||||||
weasyprint
|
weasyprint
|
||||||
markdown
|
markdown
|
||||||
pygments
|
pygments
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
|
python-dateutil
|
||||||
|
|||||||
307
server.py
307
server.py
@@ -13,26 +13,23 @@ from flask_cors import CORS
|
|||||||
import os
|
import os
|
||||||
import dotenv
|
import dotenv
|
||||||
import requests
|
import requests
|
||||||
from cloudflare import Cloudflare
|
|
||||||
import datetime
|
import datetime
|
||||||
import qrcode
|
import qrcode
|
||||||
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
|
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
|
||||||
from ansi2html import Ansi2HTMLConverter
|
from ansi2html import Ansi2HTMLConverter
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from blueprints.now import now_bp
|
# Import blueprints
|
||||||
from blueprints.blog import blog_bp
|
from blueprints import now, blog, wellknown, api, podcast, acme, spotify
|
||||||
from blueprints.wellknown import wk_bp
|
from tools import isCLI, isCrawler, getAddress, getFilePath, error_response, getClientIP, json_response, getHandshakeScript, get_tools_data
|
||||||
from blueprints.api import api_bp, getGitCommit
|
from curl import curl_response
|
||||||
from tools import isCurl, isCrawler, getAddress, getFilePath, error_response, getClientIP
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
# Register the now blueprint with the URL prefix
|
# Register blueprints
|
||||||
app.register_blueprint(now_bp, url_prefix='/now')
|
for module in [now, blog, wellknown, api, podcast, acme, spotify]:
|
||||||
app.register_blueprint(blog_bp, url_prefix='/blog')
|
app.register_blueprint(module.app)
|
||||||
app.register_blueprint(wk_bp, url_prefix='/.well-known')
|
|
||||||
app.register_blueprint(api_bp, url_prefix='/api/v1')
|
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
@@ -45,11 +42,13 @@ EMAIL_RATE_LIMIT = 3 # Max 3 requests per email per hour
|
|||||||
IP_RATE_LIMIT = 5 # Max 5 requests per IP per hour
|
IP_RATE_LIMIT = 5 # Max 5 requests per IP per hour
|
||||||
RATE_LIMIT_WINDOW = 3600 # 1 hour in seconds
|
RATE_LIMIT_WINDOW = 3600 # 1 hour in seconds
|
||||||
|
|
||||||
HANDSHAKE_SCRIPTS = '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
|
|
||||||
|
|
||||||
RESTRICTED_ROUTES = ["ascii"]
|
RESTRICTED_ROUTES = ["ascii"]
|
||||||
REDIRECT_ROUTES = {
|
REDIRECT_ROUTES = {
|
||||||
"contact": "/#contact"
|
"contact": "/#contact",
|
||||||
|
"old": "/now/old",
|
||||||
|
"/meet": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
||||||
|
"/meeting": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
||||||
|
"/appointment": "https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr",
|
||||||
}
|
}
|
||||||
DOWNLOAD_ROUTES = {
|
DOWNLOAD_ROUTES = {
|
||||||
"pgp": "data/nathanwoodburn.asc"
|
"pgp": "data/nathanwoodburn.asc"
|
||||||
@@ -71,16 +70,13 @@ NC_CONFIG = requests.get(
|
|||||||
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
|
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
if 'time-zone' not in NC_CONFIG:
|
|
||||||
NC_CONFIG['time-zone'] = 10
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Assets routes
|
# region Assets routes
|
||||||
|
|
||||||
|
|
||||||
@app.route("/assets/<path:path>")
|
@app.route("/assets/<path:path>")
|
||||||
def asset_get(path):
|
def asset(path):
|
||||||
if path.endswith(".json"):
|
if path.endswith(".json"):
|
||||||
return send_from_directory(
|
return send_from_directory(
|
||||||
"templates/assets", path, mimetype="application/json"
|
"templates/assets", path, mimetype="application/json"
|
||||||
@@ -120,7 +116,7 @@ def asset_get(path):
|
|||||||
|
|
||||||
@app.route("/sitemap")
|
@app.route("/sitemap")
|
||||||
@app.route("/sitemap.xml")
|
@app.route("/sitemap.xml")
|
||||||
def sitemap_get():
|
def sitemap():
|
||||||
# Remove all .html from sitemap
|
# Remove all .html from sitemap
|
||||||
if not os.path.isfile("templates/sitemap.xml"):
|
if not os.path.isfile("templates/sitemap.xml"):
|
||||||
return error_response(request)
|
return error_response(request)
|
||||||
@@ -132,25 +128,37 @@ def sitemap_get():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/favicon.<ext>")
|
@app.route("/favicon.<ext>")
|
||||||
def favicon_get(ext):
|
def favicon(ext):
|
||||||
if ext not in ("png", "svg", "ico"):
|
if ext not in ("png", "svg", "ico"):
|
||||||
return error_response(request)
|
return error_response(request)
|
||||||
return send_from_directory("templates/assets/img/favicon", f"favicon.{ext}")
|
return send_from_directory("templates/assets/img/favicon", f"favicon.{ext}")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<name>.js")
|
@app.route("/<name>.js")
|
||||||
def javascript_get(name):
|
def javascript(name):
|
||||||
# Check if file in js directory
|
# Check if file in js directory
|
||||||
if not os.path.isfile("templates/assets/js/" + request.path.split("/")[-1]):
|
if not os.path.isfile("templates/assets/js/" + request.path.split("/")[-1]):
|
||||||
return error_response(request)
|
return error_response(request)
|
||||||
return send_from_directory("templates/assets/js", request.path.split("/")[-1])
|
return send_from_directory("templates/assets/js", request.path.split("/")[-1])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/download/<path:path>")
|
||||||
|
def download(path):
|
||||||
|
if path not in DOWNLOAD_ROUTES:
|
||||||
|
return error_response(request, message="Invalid download")
|
||||||
|
# Check if file exists
|
||||||
|
path = DOWNLOAD_ROUTES[path]
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return send_file(path)
|
||||||
|
|
||||||
|
return error_response(request, message="File not found")
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# region PWA routes
|
# region PWA routes
|
||||||
|
|
||||||
|
|
||||||
@app.route("/manifest.json")
|
@app.route("/manifest.json")
|
||||||
def manifest_get():
|
def manifest():
|
||||||
host = request.host
|
host = request.host
|
||||||
|
|
||||||
# Read as json
|
# Read as json
|
||||||
@@ -166,48 +174,39 @@ def manifest_get():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/sw.js")
|
@app.route("/sw.js")
|
||||||
def serviceWorker_get():
|
def serviceWorker():
|
||||||
return send_from_directory("pwa", "sw.js")
|
return send_from_directory("pwa", "sw.js")
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
# region Misc routes
|
# region Misc routes
|
||||||
|
|
||||||
|
|
||||||
@app.route("/meet")
|
|
||||||
@app.route("/meeting")
|
|
||||||
@app.route("/appointment")
|
|
||||||
def meetingLink_get():
|
|
||||||
return redirect(
|
|
||||||
"https://cloud.woodburn.au/apps/calendar/appointment/PamrmmspWJZr", code=302
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/links")
|
@app.route("/links")
|
||||||
def links_get():
|
def links():
|
||||||
return render_template("link.html")
|
return render_template("link.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/<path:function>")
|
|
||||||
def api_legacy_get(function):
|
|
||||||
return redirect(f"/api/v1/{function}", code=301)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/actions.json")
|
@app.route("/actions.json")
|
||||||
def sol_actions_get():
|
def sol_actions():
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
|
{"rules": [{"pathPattern": "/donate**", "apiPath": "/api/v1/donate**"}]}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route("/api/<path:function>")
|
||||||
|
def api_legacy(function):
|
||||||
|
# Check if function is in api blueprint
|
||||||
|
for rule in app.url_map.iter_rules():
|
||||||
|
# Check if the redirect route exists
|
||||||
|
if rule.rule == f"/api/v1/{function}":
|
||||||
|
return redirect(f"/api/v1/{function}", code=301)
|
||||||
|
return error_response(request, message="404 Not Found", code=404)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Main routes
|
# region Main routes
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index_get():
|
def index():
|
||||||
global HANDSHAKE_SCRIPTS
|
|
||||||
global PROJECTS
|
global PROJECTS
|
||||||
global PROJECTS_UPDATED
|
global PROJECTS_UPDATED
|
||||||
|
|
||||||
@@ -226,15 +225,8 @@ def index_get():
|
|||||||
# Always load if load is in the query string
|
# Always load if load is in the query string
|
||||||
if request.args.get("load"):
|
if request.args.get("load"):
|
||||||
loaded = False
|
loaded = False
|
||||||
if isCurl(request):
|
if isCLI(request):
|
||||||
return jsonify(
|
return curl_response(request)
|
||||||
{
|
|
||||||
"message": "Welcome to Nathan.Woodburn/! This is a personal website. For more information, visit https://nathan.woodburn.au",
|
|
||||||
"ip": getClientIP(request),
|
|
||||||
"dev": HANDSHAKE_SCRIPTS == "",
|
|
||||||
"version": getGitCommit()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not loaded and not isCrawler(request):
|
if not loaded and not isCrawler(request):
|
||||||
# Set cookie
|
# Set cookie
|
||||||
@@ -251,7 +243,7 @@ def index_get():
|
|||||||
try:
|
try:
|
||||||
git = requests.get(
|
git = requests.get(
|
||||||
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
||||||
headers={"Authorization": os.getenv("git_token")},
|
headers={"Authorization": os.getenv("GIT_AUTH")},
|
||||||
)
|
)
|
||||||
git = git.json()
|
git = git.json()
|
||||||
git = git[0]
|
git = git[0]
|
||||||
@@ -327,14 +319,6 @@ def index_get():
|
|||||||
|
|
||||||
html_url = git["repo"]["html_url"]
|
html_url = git["repo"]["html_url"]
|
||||||
repo = '<a href="' + html_url + '" target="_blank">' + repo_name + "</a>"
|
repo = '<a href="' + html_url + '" target="_blank">' + repo_name + "</a>"
|
||||||
# If localhost, don't load handshake
|
|
||||||
if (
|
|
||||||
request.host == "localhost:5000"
|
|
||||||
or request.host == "127.0.0.1:5000"
|
|
||||||
or os.getenv("dev") == "true"
|
|
||||||
or request.host == "test.nathan.woodburn.au"
|
|
||||||
):
|
|
||||||
HANDSHAKE_SCRIPTS = ""
|
|
||||||
|
|
||||||
# Get time
|
# Get time
|
||||||
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
|
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"])
|
||||||
@@ -371,7 +355,7 @@ def index_get():
|
|||||||
resp = make_response(
|
resp = make_response(
|
||||||
render_template(
|
render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
handshake_scripts=HANDSHAKE_SCRIPTS,
|
handshake_scripts=getHandshakeScript(request.host),
|
||||||
HNS=HNSaddress,
|
HNS=HNSaddress,
|
||||||
SOL=SOLaddress,
|
SOL=SOLaddress,
|
||||||
BTC=BTCaddress,
|
BTC=BTCaddress,
|
||||||
@@ -382,7 +366,7 @@ def index_get():
|
|||||||
sites=SITES,
|
sites=SITES,
|
||||||
projects=PROJECTS,
|
projects=PROJECTS,
|
||||||
time=time,
|
time=time,
|
||||||
message=NC_CONFIG["message"],
|
message=NC_CONFIG.get("message", ""),
|
||||||
),
|
),
|
||||||
200,
|
200,
|
||||||
{"Content-Type": "text/html"},
|
{"Content-Type": "text/html"},
|
||||||
@@ -395,16 +379,9 @@ def index_get():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/donate")
|
@app.route("/donate")
|
||||||
def donate_get():
|
def donate():
|
||||||
global HANDSHAKE_SCRIPTS
|
if isCLI(request):
|
||||||
# If localhost, don't load handshake
|
return curl_response(request)
|
||||||
if (
|
|
||||||
request.host == "localhost:5000"
|
|
||||||
or request.host == "127.0.0.1:5000"
|
|
||||||
or os.getenv("dev") == "true"
|
|
||||||
or request.host == "test.nathan.woodburn.au"
|
|
||||||
):
|
|
||||||
HANDSHAKE_SCRIPTS = ""
|
|
||||||
|
|
||||||
coinList = os.listdir(".well-known/wallets")
|
coinList = os.listdir(".well-known/wallets")
|
||||||
coinList = [file for file in coinList if file[0] != "."]
|
coinList = [file for file in coinList if file[0] != "."]
|
||||||
@@ -443,7 +420,7 @@ def donate_get():
|
|||||||
)
|
)
|
||||||
return render_template(
|
return render_template(
|
||||||
"donate.html",
|
"donate.html",
|
||||||
handshake_scripts=HANDSHAKE_SCRIPTS,
|
handshake_scripts=getHandshakeScript(request.host),
|
||||||
coins=coins,
|
coins=coins,
|
||||||
default_coins=default_coins,
|
default_coins=default_coins,
|
||||||
crypto=instructions,
|
crypto=instructions,
|
||||||
@@ -513,7 +490,7 @@ def donate_get():
|
|||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"donate.html",
|
"donate.html",
|
||||||
handshake_scripts=HANDSHAKE_SCRIPTS,
|
handshake_scripts=getHandshakeScript(request.host),
|
||||||
crypto=cryptoHTML,
|
crypto=cryptoHTML,
|
||||||
coins=coins,
|
coins=coins,
|
||||||
default_coins=default_coins,
|
default_coins=default_coins,
|
||||||
@@ -521,7 +498,7 @@ def donate_get():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/address/<path:address>")
|
@app.route("/address/<path:address>")
|
||||||
def qraddress_get(address):
|
def qraddress(address):
|
||||||
qr = qrcode.QRCode(
|
qr = qrcode.QRCode(
|
||||||
version=1,
|
version=1,
|
||||||
error_correction=ERROR_CORRECT_L,
|
error_correction=ERROR_CORRECT_L,
|
||||||
@@ -542,14 +519,14 @@ def qraddress_get(address):
|
|||||||
|
|
||||||
@app.route("/qrcode/<path:data>")
|
@app.route("/qrcode/<path:data>")
|
||||||
@app.route("/qr/<path:data>")
|
@app.route("/qr/<path:data>")
|
||||||
def qrcode_get(data):
|
def qrcodee(data):
|
||||||
qr = qrcode.QRCode(
|
qr = qrcode.QRCode(
|
||||||
error_correction=ERROR_CORRECT_H, box_size=10, border=2)
|
error_correction=ERROR_CORRECT_H, box_size=10, border=2)
|
||||||
qr.add_data(data)
|
qr.add_data(data)
|
||||||
qr.make()
|
qr.make()
|
||||||
|
|
||||||
qr_image: Image.Image = qr.make_image(
|
qr_image: Image.Image = qr.make_image(
|
||||||
fill_color="black", back_color="white").convert('RGB') # type: ignore
|
fill_color="black", back_color="white").convert('RGB') # type: ignore
|
||||||
|
|
||||||
# Add logo
|
# Add logo
|
||||||
logo = Image.open("templates/assets/img/favicon/logo.png")
|
logo = Image.open("templates/assets/img/favicon/logo.png")
|
||||||
@@ -568,7 +545,7 @@ def qrcode_get(data):
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/supersecretpath")
|
@app.route("/supersecretpath")
|
||||||
def supersecretpath_get():
|
def supersecretpath():
|
||||||
ascii_art = ""
|
ascii_art = ""
|
||||||
if os.path.isfile("data/ascii.txt"):
|
if os.path.isfile("data/ascii.txt"):
|
||||||
with open("data/ascii.txt") as file:
|
with open("data/ascii.txt") as file:
|
||||||
@@ -579,32 +556,20 @@ def supersecretpath_get():
|
|||||||
return render_template("ascii.html", ascii_art=ascii_art_html)
|
return render_template("ascii.html", ascii_art=ascii_art_html)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/download/<path:path>")
|
|
||||||
def download_get(path):
|
|
||||||
if path not in DOWNLOAD_ROUTES:
|
|
||||||
return error_response(request, message="Invalid download")
|
|
||||||
# Check if file exists
|
|
||||||
path = DOWNLOAD_ROUTES[path]
|
|
||||||
if os.path.isfile(path):
|
|
||||||
return send_file(path)
|
|
||||||
|
|
||||||
return error_response(request, message="File not found")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/hosting/send-enquiry", methods=["POST"])
|
@app.route("/hosting/send-enquiry", methods=["POST"])
|
||||||
def hosting_post():
|
def hosting_post():
|
||||||
global EMAIL_REQUEST_COUNT
|
global EMAIL_REQUEST_COUNT
|
||||||
global IP_REQUEST_COUNT
|
global IP_REQUEST_COUNT
|
||||||
|
|
||||||
if not request.json:
|
if not request.is_json or not request.json:
|
||||||
return jsonify({"status": "error", "message": "No JSON data provided"}), 400
|
return json_response(request, "No JSON data provided", 415)
|
||||||
|
|
||||||
# Keys
|
# Keys
|
||||||
# email, cpus, memory, disk, backups, message
|
# email, cpus, memory, disk, backups, message
|
||||||
required_keys = ["email", "cpus", "memory", "disk", "backups", "message"]
|
required_keys = ["email", "cpus", "memory", "disk", "backups", "message"]
|
||||||
for key in required_keys:
|
for key in required_keys:
|
||||||
if key not in request.json:
|
if key not in request.json:
|
||||||
return jsonify({"status": "error", "message": f"Missing key: {key}"}), 400
|
return json_response(request, f"Missing key: {key}", 400)
|
||||||
|
|
||||||
email = request.json["email"]
|
email = request.json["email"]
|
||||||
ip = getClientIP(request)
|
ip = getClientIP(request)
|
||||||
@@ -623,10 +588,7 @@ def hosting_post():
|
|||||||
# Increment counter
|
# Increment counter
|
||||||
EMAIL_REQUEST_COUNT[email]["count"] += 1
|
EMAIL_REQUEST_COUNT[email]["count"] += 1
|
||||||
if EMAIL_REQUEST_COUNT[email]["count"] > EMAIL_RATE_LIMIT:
|
if EMAIL_REQUEST_COUNT[email]["count"] > EMAIL_RATE_LIMIT:
|
||||||
return jsonify({
|
return json_response(request, "Rate limit exceeded. Please try again later.", 429)
|
||||||
"status": "error",
|
|
||||||
"message": "Rate limit exceeded. Please try again later."
|
|
||||||
}), 429
|
|
||||||
else:
|
else:
|
||||||
# First request for this email
|
# First request for this email
|
||||||
EMAIL_REQUEST_COUNT[email] = {"count": 1, "last_reset": current_time}
|
EMAIL_REQUEST_COUNT[email] = {"count": 1, "last_reset": current_time}
|
||||||
@@ -640,10 +602,7 @@ def hosting_post():
|
|||||||
# Increment counter
|
# Increment counter
|
||||||
IP_REQUEST_COUNT[ip]["count"] += 1
|
IP_REQUEST_COUNT[ip]["count"] += 1
|
||||||
if IP_REQUEST_COUNT[ip]["count"] > IP_RATE_LIMIT:
|
if IP_REQUEST_COUNT[ip]["count"] > IP_RATE_LIMIT:
|
||||||
return jsonify({
|
return json_response(request, "Rate limit exceeded. Please try again later.", 429)
|
||||||
"status": "error",
|
|
||||||
"message": "Rate limit exceeded. Please try again later."
|
|
||||||
}), 429
|
|
||||||
else:
|
else:
|
||||||
# First request for this IP
|
# First request for this IP
|
||||||
IP_REQUEST_COUNT[ip] = {"count": 1, "last_reset": current_time}
|
IP_REQUEST_COUNT[ip] = {"count": 1, "last_reset": current_time}
|
||||||
@@ -663,26 +622,27 @@ def hosting_post():
|
|||||||
message = str(message)
|
message = str(message)
|
||||||
email = str(email)
|
email = str(email)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return jsonify({"status": "error", "message": "Invalid data types"}), 400
|
return json_response(request, "Invalid data types", 400)
|
||||||
|
|
||||||
# Basic validation
|
# Basic validation
|
||||||
if not isinstance(cpus, int) or cpus < 1 or cpus > 64:
|
if not isinstance(cpus, int) or cpus < 1 or cpus > 64:
|
||||||
return jsonify({"status": "error", "message": "Invalid CPUs"}), 400
|
return json_response(request, "Invalid CPUs", 400)
|
||||||
if not isinstance(memory, float) or memory < 0.5 or memory > 512:
|
if not isinstance(memory, float) or memory < 0.5 or memory > 512:
|
||||||
return jsonify({"status": "error", "message": "Invalid memory"}), 400
|
return json_response(request, "Invalid memory", 400)
|
||||||
if not isinstance(disk, int) or disk < 10 or disk > 500:
|
if not isinstance(disk, int) or disk < 10 or disk > 500:
|
||||||
return jsonify({"status": "error", "message": "Invalid disk"}), 400
|
return json_response(request, "Invalid disk", 400)
|
||||||
if not isinstance(backups, bool):
|
if not isinstance(backups, bool):
|
||||||
return jsonify({"status": "error", "message": "Invalid backups"}), 400
|
return json_response(request, "Invalid backups", 400)
|
||||||
if not isinstance(message, str) or len(message) > 1000:
|
if not isinstance(message, str) or len(message) > 1000:
|
||||||
return jsonify({"status": "error", "message": "Invalid message"}), 400
|
return json_response(request, "Invalid message", 400)
|
||||||
if not isinstance(email, str) or len(email) > 100 or "@" not in email:
|
if not isinstance(email, str) or len(email) > 100 or "@" not in email:
|
||||||
return jsonify({"status": "error", "message": "Invalid email"}), 400
|
return json_response(request, "Invalid email", 400)
|
||||||
|
|
||||||
# Send to Discord webhook
|
# Send to Discord webhook
|
||||||
webhook_url = os.getenv("HOSTING_WEBHOOK")
|
webhook_url = os.getenv("HOSTING_WEBHOOK")
|
||||||
if not webhook_url:
|
if not webhook_url:
|
||||||
return jsonify({"status": "error", "message": "Hosting webhook not set"}), 500
|
return json_response(request, "Hosting webhook not set", 500)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"content": "",
|
"content": "",
|
||||||
"embeds": [
|
"embeds": [
|
||||||
@@ -698,143 +658,54 @@ def hosting_post():
|
|||||||
}
|
}
|
||||||
response = requests.post(webhook_url, json=data, headers=headers)
|
response = requests.post(webhook_url, json=data, headers=headers)
|
||||||
if response.status_code != 204 and response.status_code != 200:
|
if response.status_code != 204 and response.status_code != 200:
|
||||||
return jsonify({"status": "error", "message": "Failed to send enquiry"}), 500
|
return json_response(request, "Failed to send enquiry", 500)
|
||||||
return jsonify({"status": "success", "message": "Enquiry sent successfully"}), 200
|
return json_response(request, "Enquiry sent", 200)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/resume.pdf")
|
@app.route("/resume.pdf")
|
||||||
def resume_pdf_get():
|
def resume_pdf():
|
||||||
# Check if file exists
|
# Check if file exists
|
||||||
if os.path.isfile("data/resume.pdf"):
|
if os.path.isfile("data/resume.pdf"):
|
||||||
return send_file("data/resume.pdf")
|
return send_file("data/resume.pdf")
|
||||||
return error_response(request, message="Resume not found")
|
return error_response(request, message="Resume not found")
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region ACME route
|
@app.route("/tools")
|
||||||
|
def tools():
|
||||||
|
if isCLI(request):
|
||||||
@app.route("/hnsdoh-acme", methods=["POST"])
|
return curl_response(request)
|
||||||
def acme_post():
|
return render_template("tools.html", tools=get_tools_data())
|
||||||
print(f"ACME request from {getClientIP(request)}")
|
|
||||||
|
|
||||||
# Get the TXT record from the request
|
|
||||||
if not request.json:
|
|
||||||
print("No JSON data provided for ACME")
|
|
||||||
return jsonify({"status": "error", "error": "No JSON data provided"})
|
|
||||||
if "txt" not in request.json or "auth" not in request.json:
|
|
||||||
print("Missing required data for ACME")
|
|
||||||
return jsonify({"status": "error", "error": "Missing required data"})
|
|
||||||
|
|
||||||
txt = request.json["txt"]
|
|
||||||
auth = request.json["auth"]
|
|
||||||
if auth != os.getenv("CF_AUTH"):
|
|
||||||
print("Invalid auth for ACME")
|
|
||||||
return jsonify({"status": "error", "error": "Invalid auth"})
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
print(f"ACME request successful: {txt}")
|
|
||||||
return jsonify({"status": "success"})
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Podcast routes
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/ID1")
|
|
||||||
def podcast_index_get():
|
|
||||||
# Proxy to ID1 url
|
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/ID1")
|
|
||||||
return make_response(
|
|
||||||
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/ID1/")
|
|
||||||
def podcast_contents_get():
|
|
||||||
# Proxy to ID1 url
|
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/ID1/")
|
|
||||||
return make_response(
|
|
||||||
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/ID1/<path:path>")
|
|
||||||
def podcast_path_get(path):
|
|
||||||
# Proxy to ID1 url
|
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/ID1/" + path)
|
|
||||||
return make_response(
|
|
||||||
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/ID1.xml")
|
|
||||||
def podcast_xml_get():
|
|
||||||
# Proxy to ID1 url
|
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/ID1.xml")
|
|
||||||
return make_response(
|
|
||||||
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/podsync.opml")
|
|
||||||
def podcast_podsync_get():
|
|
||||||
req = requests.get("https://podcasts.c.woodburn.au/podsync.opml")
|
|
||||||
return make_response(
|
|
||||||
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
|
||||||
)
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region Error Catching
|
# region Error Catching
|
||||||
|
|
||||||
# Catch all for GET requests
|
# Catch all for GET requests
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def catch_all_get(path: str):
|
def catch_all(path: str):
|
||||||
global HANDSHAKE_SCRIPTS
|
|
||||||
# If localhost, don't load handshake
|
|
||||||
if (
|
|
||||||
request.host == "localhost:5000"
|
|
||||||
or request.host == "127.0.0.1:5000"
|
|
||||||
or os.getenv("dev") == "true"
|
|
||||||
or request.host == "test.nathan.woodburn.au"
|
|
||||||
):
|
|
||||||
HANDSHAKE_SCRIPTS = ""
|
|
||||||
|
|
||||||
if path.lower().replace(".html", "") in RESTRICTED_ROUTES:
|
if path.lower().replace(".html", "") in RESTRICTED_ROUTES:
|
||||||
return error_response(request, message="Restricted route", code=403)
|
return error_response(request, message="Restricted route", code=403)
|
||||||
|
|
||||||
|
# If curl request, return curl response
|
||||||
|
if isCLI(request):
|
||||||
|
return curl_response(request)
|
||||||
|
|
||||||
if path in REDIRECT_ROUTES:
|
if path in REDIRECT_ROUTES:
|
||||||
return redirect(REDIRECT_ROUTES[path], code=302)
|
return redirect(REDIRECT_ROUTES[path], code=302)
|
||||||
|
|
||||||
# If file exists, load it
|
# If file exists, load it
|
||||||
if os.path.isfile("templates/" + path):
|
if os.path.isfile("templates/" + path):
|
||||||
return render_template(path, handshake_scripts=HANDSHAKE_SCRIPTS, sites=SITES)
|
return render_template(path, handshake_scripts=getHandshakeScript(request.host), sites=SITES)
|
||||||
|
|
||||||
# Try with .html
|
# Try with .html
|
||||||
if os.path.isfile("templates/" + path + ".html"):
|
if os.path.isfile("templates/" + path + ".html"):
|
||||||
return render_template(
|
return render_template(
|
||||||
path + ".html", handshake_scripts=HANDSHAKE_SCRIPTS, sites=SITES
|
path + ".html", handshake_scripts=getHandshakeScript(request.host), sites=SITES
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.isfile("templates/" + path.strip("/") + ".html"):
|
if os.path.isfile("templates/" + path.strip("/") + ".html"):
|
||||||
return render_template(
|
return render_template(
|
||||||
path.strip("/") + ".html", handshake_scripts=HANDSHAKE_SCRIPTS, sites=SITES
|
path.strip("/") + ".html", handshake_scripts=getHandshakeScript(request.host), sites=SITES
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to find a file matching
|
# Try to find a file matching
|
||||||
@@ -846,9 +717,13 @@ def catch_all_get(path: str):
|
|||||||
|
|
||||||
return error_response(request)
|
return error_response(request)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
return error_response(request)
|
return error_response(request)
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, port=5000, host="127.0.0.1")
|
app.run(debug=True, port=5000, host="127.0.0.1")
|
||||||
|
|||||||
47
sol.py
47
sol.py
@@ -1,47 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
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)
|
|
||||||
2
templates/assets/css/brand-reveal.min.css
vendored
2
templates/assets/css/brand-reveal.min.css
vendored
@@ -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}
|
||||||
2
templates/assets/css/styles.min.css
vendored
2
templates/assets/css/styles.min.css
vendored
@@ -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}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.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}}
|
||||||
1
templates/assets/css/tools.min.css
vendored
Normal file
1
templates/assets/css/tools.min.css
vendored
Normal 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}
|
||||||
BIN
templates/assets/img/external/spotify.png
vendored
Normal file
BIN
templates/assets/img/external/spotify.png
vendored
Normal file
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 |
BIN
templates/assets/img/profile.webp
Normal file
BIN
templates/assets/img/profile.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -55,6 +55,7 @@ Find something interesting to read. Or maybe check one of my tutorials">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ Find something interesting to read. Or maybe check one of my tutorials">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
14
templates/contact.ascii
Normal file
14
templates/contact.ascii
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m CONTACT ME [0m
|
||||||
|
[1;36m────────────[0m
|
||||||
|
|
||||||
|
Here are my socials — I’m 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
25
templates/donate.ascii
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m DONATE [0m
|
||||||
|
[1;36m────────[0m
|
||||||
|
|
||||||
|
If you’d like to support my work 💙
|
||||||
|
|
||||||
|
- PayPal: https://paypal.me/nathanwoodburn
|
||||||
|
- GitHub: https://github.com/sponsors/Nathanwoodburn
|
||||||
|
- Stripe: https://donate.stripe.com/8wM6pv0VD08Xe408ww
|
||||||
|
|
||||||
|
[1mHNS: nathan.woodburn[0m
|
||||||
|
[1m{{ HNS }}[0m
|
||||||
|
|
||||||
|
[1mBTC: thinbadger6@primal.net[0m
|
||||||
|
[1m{{ BTC }}[0m
|
||||||
|
|
||||||
|
[1mSOL: woodburn.sol[0m
|
||||||
|
[1m{{ SOL }}[0m
|
||||||
|
|
||||||
|
[1mETH: woodburn.au[0m
|
||||||
|
[1m{{ ETH }}[0m
|
||||||
|
|
||||||
|
More donation options → [/donate/more]
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
10
templates/donate_coin.ascii
Normal file
10
templates/donate_coin.ascii
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m DONATE [0m
|
||||||
|
[1;36m────────[0m
|
||||||
|
|
||||||
|
Here is my [1m{{ coin }}[0m address if you'd like to send a donation 💙
|
||||||
|
[1m{{ address }}[0m
|
||||||
|
|
||||||
|
Thank you for your support! 🙏
|
||||||
|
|
||||||
13
templates/donate_more.ascii
Normal file
13
templates/donate_more.ascii
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m DONATE [0m
|
||||||
|
[1;36m────────[0m
|
||||||
|
|
||||||
|
Here is a list of additional cryptocurrencies and donation methods 💙
|
||||||
|
For each coin below, you can get the address from [1m/donate/<coin>[0m
|
||||||
|
|
||||||
|
{% for coin in coins %}{% if loop.index0 % 4 == 0 and loop.index0 != 0 %}
|
||||||
|
{% endif %}[1m{{ coin }}[0m{% if not loop.last %}, {% endif %}{% endfor %}
|
||||||
|
|
||||||
|
Thank you for your support! 🙏
|
||||||
|
|
||||||
9
templates/error.ascii
Normal file
9
templates/error.ascii
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;31m ERROR: {{ error.code }} [0m
|
||||||
|
[1;36m────────────[0m
|
||||||
|
|
||||||
|
[1;31m{{ error.message }}[0m
|
||||||
|
|
||||||
|
If you believe this is an error, please contact me via my socials listed at /contact
|
||||||
|
|
||||||
25
templates/favicon.ascii
Normal file
25
templates/favicon.ascii
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
▒▒▒ ▓▓▓
|
||||||
|
▒░░░░▒▓ ▓▓▓▓▓▓▓
|
||||||
|
▒░░░░░░▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒░░░░░▒▒▒▒▒▒▒ ▓▓▒▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒░░░▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒░░▒▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒ ▒▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒ ▒▒▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒▒▒ ▒▒▒▒▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒▒▒▒▒▒ ▒▒▒▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▒▒▒▒▒▒▒▒▒▒▒ ▒▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█
|
||||||
|
▓▒▒▒▒▒▒▒▒▒▒▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||||
|
▓▓▒▒▒▒▓▓▓ ▓▓▓▓▓▓▓▓█
|
||||||
|
▓▓▓▓ ▓▓▓█
|
||||||
|
|
||||||
13
templates/header.ascii
Normal file
13
templates/header.ascii
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[1;36m─────────────────────────────────────────────────────[0m
|
||||||
|
[1;36m . . , . . . .. / [0m
|
||||||
|
[1;36m |\ | _.-+-|_ _.._ | | _ _ _||_ . .._.._ / [0m
|
||||||
|
[1;36m | \|(_] | [ )(_][ ) * |/\|(_)(_)(_][_)(_|[ [ )/ [0m
|
||||||
|
[1;36m─────────────────────────────────────────────────────[0m
|
||||||
|
|
||||||
|
Home [/]
|
||||||
|
Contact [/contact]
|
||||||
|
Projects [/projects]
|
||||||
|
Tools [/tools]
|
||||||
|
Donate [/donate]
|
||||||
|
Now [/now]
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
46
templates/index.ascii
Normal file
46
templates/index.ascii
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
[1;36m─────────────────────────────────────────────────────[0m
|
||||||
|
[1;36m . . , . . . .. / [0m
|
||||||
|
[1;36m |\ | _.-+-|_ _.._ | | _ _ _||_ . .._.._ / [0m
|
||||||
|
[1;36m | \|(_] | [ )(_][ ) * |/\|(_)(_)(_][_)(_|[ [ )/ [0m
|
||||||
|
[1;36m─────────────────────────────────────────────────────[0m
|
||||||
|
|
||||||
|
Home [/]
|
||||||
|
Contact [/contact]
|
||||||
|
Projects [/projects]
|
||||||
|
Tools [/tools]
|
||||||
|
Donate [/donate]
|
||||||
|
Now [/now]
|
||||||
|
API [/api/v1]
|
||||||
|
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m ABOUT ME [0m
|
||||||
|
[1;36m──────────[0m
|
||||||
|
|
||||||
|
Hi, I'm [1mNathan Woodburn[0m from Canberra, Australia.
|
||||||
|
I've been homeschooled through Year 12 and am now studying a
|
||||||
|
[1mBachelor of Computer Science[0m.
|
||||||
|
|
||||||
|
I love building random projects, so this site is always evolving.
|
||||||
|
I'm also one of the founders of [1;36mHandshake AU[0m [https://hns.au],
|
||||||
|
working to grow Handshake adoption across Australia.
|
||||||
|
|
||||||
|
I'm currently working on: {{ repo | safe }}
|
||||||
|
{% if not spotify.message %}Currently listening to: [1;36m{{ spotify.song_name }}[0m by [1;36m{{ spotify.artist }}[0m{% endif %}
|
||||||
|
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m SKILLS [0m
|
||||||
|
[1;36m────────[0m
|
||||||
|
|
||||||
|
- Linux servers & CLI
|
||||||
|
- DNS & DNSSEC
|
||||||
|
- NGINX web servers
|
||||||
|
- Programming:
|
||||||
|
- Python 3
|
||||||
|
- C#
|
||||||
|
- Java
|
||||||
|
- Bash
|
||||||
|
|
||||||
|
|
||||||
|
Served to: {{ ip }}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
|
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
|
||||||
<link rel="stylesheet" href="/assets/css/swiper.min.css">
|
<link rel="stylesheet" href="/assets/css/swiper.min.css">
|
||||||
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
|
<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>
|
</head>
|
||||||
|
|
||||||
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="77"><script>
|
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="77"><script>
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -78,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>
|
<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>
|
Some services are down.<br>
|
||||||
Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.webp"></div>
|
Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.webp"></div>
|
||||||
<header class="masthead main" style="background: url("/assets/img/bg/BlueMountains.jpg") center / cover;position: relative;height: 400px;">
|
<header class="masthead main" style="position: relative;height: 400px;background: url("/assets/img/bg/BlueMountains.jpg") center / cover;">
|
||||||
<div class="intro-body text parallax">
|
<div class="intro-body text parallax">
|
||||||
<div class="name-container" style="padding-right: 1em;">
|
<div class="name-container" style="padding-right: 1em;">
|
||||||
<div class="slider">
|
<div class="slider">
|
||||||
@@ -94,7 +95,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-8 mx-auto">
|
<div class="col-lg-8 mx-auto">
|
||||||
<h2>About ME</h2>
|
<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>
|
<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 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> working to increase Handshake adoption in Australia.</p>
|
<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 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> 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 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>
|
<p data-bs-toggle="tooltip" data-bss-tooltip="" title="{{repo_description}}" style="display: inline-block;">{{repo | safe}}</p>
|
||||||
@@ -105,7 +106,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
|
|||||||
<h2>Skills</h2>
|
<h2>Skills</h2>
|
||||||
<ul class="list-unstyled" style="font-size: 18px;">
|
<ul class="list-unstyled" style="font-size: 18px;">
|
||||||
<li class="programlinux">Linux Servers and CLI</li>
|
<li class="programlinux">Linux Servers and CLI</li>
|
||||||
<li>DNS, DNSSEC and Trustless SSL</li>
|
<li>DNS and DNSSEC</li>
|
||||||
<li class="programnginx">NGINX Web Servers</li>
|
<li class="programnginx">NGINX Web Servers</li>
|
||||||
<li class="programc">Programming in<ul class="list-inline">
|
<li class="programc">Programming in<ul class="list-inline">
|
||||||
<li class="list-inline-item">Python 3</li>
|
<li class="list-inline-item">Python 3</li>
|
||||||
@@ -126,7 +127,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
|
|||||||
<div class="swiper">
|
<div class="swiper">
|
||||||
<div class="swiper-wrapper">{% for project in projects %}
|
<div class="swiper-wrapper">{% for project in projects %}
|
||||||
<div class="swiper-slide site" data-url="{{ project.html_url }}">
|
<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-body">
|
||||||
<div class="site-detail" style="width: 100%;">
|
<div class="site-detail" style="width: 100%;">
|
||||||
<h2 class="site-name" style="text-align: left;">{{ project.name }}</h2>
|
<h2 class="site-name" style="text-align: left;">{{ project.name }}</h2>
|
||||||
@@ -227,7 +228,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
|
|||||||
<div class="container text-center">
|
<div class="container text-center">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<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>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -291,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">
|
<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>
|
<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/bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script src="/assets/js/script.min.js"></script>
|
<script src="/assets/js/script.min.js"></script>
|
||||||
<script src="/assets/js/grayscale.min.js"></script>
|
<script src="/assets/js/grayscale.min.js"></script>
|
||||||
|
|||||||
6
templates/now.ascii
Normal file
6
templates/now.ascii
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m Now {{ date }} [0m
|
||||||
|
[1;36m────────────[0m
|
||||||
|
|
||||||
|
{{content | safe}}
|
||||||
@@ -56,6 +56,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last little bit">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Find out what I've been up to in the last little bit">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
173
templates/now/25_10_23.html
Normal file
173
templates/now/25_10_23.html
Normal 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&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&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("/assets/img/bg/background.webp") 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> at <a href="https://nathan.woodburn">https://nathan.woodburn/</a></p>
|
||||||
|
<p class="copyright">Copyright © 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>
|
||||||
@@ -56,6 +56,7 @@ Find out what I've been up to in the last week">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ Find out what I've been up to in the last little bit">
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
9
templates/projects.ascii
Normal file
9
templates/projects.ascii
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m RECENT PROJECTS [0m
|
||||||
|
[1;36m─────────────────[0m
|
||||||
|
|
||||||
|
{{projects}}
|
||||||
|
|
||||||
|
Look at more projects on my Git: [1;36mhttps://git.woodburn.au[0m
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
<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="/">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="/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="/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="/blog">Blog</a></li>
|
||||||
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
<url>
|
<url>
|
||||||
<loc>https://nathan.woodburn.au/now/25_08_15</loc>
|
<loc>https://nathan.woodburn.au/now/25_08_15</loc>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nathan.woodburn.au/now/25_10_23</loc>
|
||||||
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://nathan.woodburn.au/now/old</loc>
|
<loc>https://nathan.woodburn.au/now/old</loc>
|
||||||
</url>
|
</url>
|
||||||
@@ -93,4 +96,7 @@
|
|||||||
<url>
|
<url>
|
||||||
<loc>https://nathan.woodburn.au/resume</loc>
|
<loc>https://nathan.woodburn.au/resume</loc>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://nathan.woodburn.au/tools</loc>
|
||||||
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
20
templates/tools.ascii
Normal file
20
templates/tools.ascii
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{{header}}
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
[1;36m Tools [0m
|
||||||
|
[1;36m────────────[0m
|
||||||
|
|
||||||
|
Here are some of the tools I use regularly — most of them are open source! 🛠️
|
||||||
|
|
||||||
|
{% for type, tools_in_type in tools | groupby('type') %}
|
||||||
|
[4m[1;33m{{type}}[0m
|
||||||
|
{% for tool in tools_in_type %}
|
||||||
|
[1;33m{{tool.name}}[0m
|
||||||
|
{{tool.description}}
|
||||||
|
Website: {{tool.url}}
|
||||||
|
{% if tool.demo %}Demo: {{tool.demo}}{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
[1;36m───────────────────────────────────────────────[0m
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
215
templates/tools.html
Normal file
215
templates/tools.html
Normal 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&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&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("/assets/img/bg/projects.webp") 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 © 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
3
tests/README.md
Normal 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
28
tests/api.hurl
Normal 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" == "127.0.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
9
tests/blog.hurl
Normal 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
41
tests/legacy_api.hurl
Normal 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
24
tests/now.hurl
Normal 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
14
tests/sol.slow_hurl
Normal 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
3
tests/test.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
hurl --test *.hurl
|
||||||
11
tests/well-known.hurl
Normal file
11
tests/well-known.hurl
Normal 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
|
||||||
|
|
||||||
273
tools.py
273
tools.py
@@ -1,74 +1,279 @@
|
|||||||
from flask import Request, render_template, jsonify
|
from flask import Request, render_template, jsonify, make_response
|
||||||
import os
|
import os
|
||||||
from functools import cache
|
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
|
||||||
|
|
||||||
def getClientIP(request):
|
# 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")
|
x_forwarded_for = request.headers.get("X-Forwarded-For")
|
||||||
if x_forwarded_for:
|
if x_forwarded_for:
|
||||||
ip = x_forwarded_for.split(",")[0]
|
ip = x_forwarded_for.split(",")[0]
|
||||||
else:
|
else:
|
||||||
ip = request.remote_addr
|
ip = request.remote_addr
|
||||||
|
if ip is None:
|
||||||
|
ip = "unknown"
|
||||||
return ip
|
return ip
|
||||||
|
|
||||||
def isCurl(request: Request) -> bool:
|
@cache
|
||||||
|
def getGitCommit() -> str:
|
||||||
"""
|
"""
|
||||||
Check if the request is from curl
|
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:
|
Args:
|
||||||
request (Request): The Flask request object
|
request (Request): The Flask request object
|
||||||
Returns:
|
|
||||||
bool: True if the request is from curl, False otherwise
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the request is from curl or hurl, False otherwise
|
||||||
"""
|
"""
|
||||||
if request.headers and request.headers.get("User-Agent"):
|
if request.headers and request.headers.get("User-Agent"):
|
||||||
# Check if curl
|
user_agent = request.headers.get("User-Agent", "")
|
||||||
if "curl" in request.headers.get("User-Agent", "curl"):
|
return any(agent in user_agent for agent in CLI_AGENTS)
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def isCrawler(request: Request) -> bool:
|
def isCrawler(request: Request) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the request is from a web crawler (e.g., Googlebot, Bingbot)
|
Check if the request is from a web crawler (e.g., Googlebot, Bingbot).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (Request): The Flask request object
|
request (Request): The Flask request object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the request is from a web crawler, False otherwise
|
bool: True if the request is from a web crawler, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if request.headers and request.headers.get("User-Agent"):
|
if request.headers and request.headers.get("User-Agent"):
|
||||||
# Check if Googlebot or Bingbot
|
user_agent = request.headers.get("User-Agent", "")
|
||||||
if "Googlebot" in request.headers.get(
|
return any(crawler in user_agent for crawler in CRAWLERS)
|
||||||
"User-Agent", ""
|
|
||||||
) or "Bingbot" in request.headers.get("User-Agent", ""):
|
|
||||||
return True
|
|
||||||
return False
|
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
|
@cache
|
||||||
def getAddress(coin: str) -> str:
|
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 = ""
|
address = ""
|
||||||
if os.path.isfile(".well-known/wallets/" + coin.upper()):
|
wallet_path = f".well-known/wallets/{coin.upper()}"
|
||||||
with open(".well-known/wallets/" + coin.upper()) as file:
|
if os.path.isfile(wallet_path):
|
||||||
|
with open(wallet_path) as file:
|
||||||
address = file.read()
|
address = file.read()
|
||||||
return address
|
return address
|
||||||
|
|
||||||
|
|
||||||
def getFilePath(name, path):
|
@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):
|
for root, dirs, files in os.walk(path):
|
||||||
if name in files:
|
if name in files:
|
||||||
return os.path.join(root, name)
|
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)
|
||||||
|
|
||||||
def error_response(request: Request, message: str = "404 Not Found", code: int = 404):
|
|
||||||
if isCurl(request):
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"status": code,
|
|
||||||
"message": message,
|
|
||||||
"ip": getClientIP(request),
|
|
||||||
}
|
|
||||||
), code
|
|
||||||
|
|
||||||
# Check if <error code>.html exists in templates
|
# Check if <error code>.html exists in templates
|
||||||
if os.path.isfile(f"templates/{code}.html"):
|
template_name = f"{code}.html" if os.path.isfile(
|
||||||
return render_template(f"{code}.html"), code
|
f"templates/{code}.html") else "404.html"
|
||||||
return render_template("404.html"), code
|
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)
|
||||||
Reference in New Issue
Block a user