4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
a78d999a61 Fix trailing whitespace and finalize performance improvements
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 51s
Build Docker / BuildImage (push) Successful in 59s
Co-authored-by: Nathanwoodburn <62039630+Nathanwoodburn@users.noreply.github.com>
2025-11-21 11:34:51 +00:00
copilot-swe-agent[bot]
74afdc1b5b Add caching to blog and now blueprints for improved performance
Co-authored-by: Nathanwoodburn <62039630+Nathanwoodburn@users.noreply.github.com>
2025-11-21 11:31:49 +00:00
copilot-swe-agent[bot]
607fdd4d46 Add caching layer for expensive API calls and file operations
Co-authored-by: Nathanwoodburn <62039630+Nathanwoodburn@users.noreply.github.com>
2025-11-21 11:30:23 +00:00
copilot-swe-agent[bot]
ca01b96e80 Initial plan 2025-11-21 11:21:23 +00:00
7 changed files with 365 additions and 217 deletions

View File

@@ -8,6 +8,7 @@ from tools import getClientIP, getGitCommit, json_response, parse_date, get_tool
from blueprints import sol from blueprints import sol
from dateutil import parser as date_parser from dateutil import parser as date_parser
from blueprints.spotify import get_spotify_track from blueprints.spotify import get_spotify_track
from cache_helper import get_nc_config, get_git_latest_activity
# Constants # Constants
HTTP_OK = 200 HTTP_OK = 200
@@ -21,14 +22,6 @@ app = Blueprint('api', __name__, url_prefix='/api/v1')
# Register solana blueprint # Register solana blueprint
app.register_blueprint(sol.app) app.register_blueprint(sol.app)
# Load configuration
NC_CONFIG = requests.get(
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
).json()
if 'time-zone' not in NC_CONFIG:
NC_CONFIG['time-zone'] = 10
@app.route("/", strict_slashes=False) @app.route("/", strict_slashes=False)
@app.route("/help") @app.route("/help")
@@ -71,13 +64,14 @@ def version():
@app.route("/time") @app.route("/time")
def time(): def time():
"""Get the current time in the configured timezone.""" """Get the current time in the configured timezone."""
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"]) nc_config = get_nc_config()
timezone_offset = datetime.timedelta(hours=nc_config["time-zone"])
timezone = datetime.timezone(offset=timezone_offset) timezone = datetime.timezone(offset=timezone_offset)
current_time = datetime.datetime.now(tz=timezone) current_time = datetime.datetime.now(tz=timezone)
return jsonify({ return jsonify({
"timestring": current_time.strftime("%A, %B %d, %Y %I:%M %p"), "timestring": current_time.strftime("%A, %B %d, %Y %I:%M %p"),
"timestamp": current_time.timestamp(), "timestamp": current_time.timestamp(),
"timezone": NC_CONFIG["time-zone"], "timezone": nc_config["time-zone"],
"timeISO": current_time.isoformat(), "timeISO": current_time.isoformat(),
"ip": getClientIP(request), "ip": getClientIP(request),
"status": HTTP_OK "status": HTTP_OK
@@ -87,8 +81,9 @@ def time():
@app.route("/timezone") @app.route("/timezone")
def timezone(): def timezone():
"""Get the current timezone setting.""" """Get the current timezone setting."""
nc_config = get_nc_config()
return jsonify({ return jsonify({
"timezone": NC_CONFIG["time-zone"], "timezone": nc_config["time-zone"],
"ip": getClientIP(request), "ip": getClientIP(request),
"status": HTTP_OK "status": HTTP_OK
}) })
@@ -97,8 +92,9 @@ def timezone():
@app.route("/message") @app.route("/message")
def message(): def message():
"""Get the message from the configuration.""" """Get the message from the configuration."""
nc_config = get_nc_config()
return jsonify({ return jsonify({
"message": NC_CONFIG["message"], "message": nc_config["message"],
"ip": getClientIP(request), "ip": getClientIP(request),
"status": HTTP_OK "status": HTTP_OK
}) })
@@ -138,27 +134,16 @@ def email_post():
@app.route("/project") @app.route("/project")
def project(): def project():
"""Get information about the current git project.""" """Get information about the current git project."""
gitinfo = { git = get_git_latest_activity()
"website": None, repo_name = git["repo"]["name"].lower()
}
try:
git = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
headers={"Authorization": os.getenv("git_token")},
)
git = git.json()
git = git[0]
repo_name = git["repo"]["name"]
repo_name = repo_name.lower()
repo_description = git["repo"]["description"] repo_description = git["repo"]["description"]
gitinfo["name"] = repo_name
gitinfo["description"] = repo_description gitinfo = {
gitinfo["url"] = git["repo"]["html_url"] "name": repo_name,
if "website" in git["repo"]: "description": repo_description,
gitinfo["website"] = git["repo"]["website"] "url": git["repo"]["html_url"],
except Exception as e: "website": git["repo"].get("website"),
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,

View File

@@ -3,11 +3,13 @@ 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 functools import lru_cache
from tools import isCLI, getClientIP, getHandshakeScript from tools import isCLI, getClientIP, getHandshakeScript
app = Blueprint('blog', __name__, url_prefix='/blog') app = Blueprint('blog', __name__, url_prefix='/blog')
@lru_cache(maxsize=32)
def list_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 # Sort pages by modified time, newest first
@@ -21,28 +23,43 @@ def list_page_files():
return blog_pages return blog_pages
def render_page(date, handshake_scripts=None): @lru_cache(maxsize=64)
# Convert md to html def get_blog_content(date):
"""Get and cache blog content."""
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 None
with open(f"data/blog/{date}.md", "r") as f: with open(f"data/blog/{date}.md", "r") as f:
content = f.read() return f.read()
# Get the title from the file name
title = date.removesuffix(".md").replace("_", " ")
# Convert the md to html @lru_cache(maxsize=64)
content = markdown.markdown( def render_markdown_to_html(content):
"""Convert markdown to HTML with caching."""
html = markdown.markdown(
content, extensions=['sane_lists', 'codehilite', 'fenced_code']) content, extensions=['sane_lists', 'codehilite', 'fenced_code'])
# Add target="_blank" to all links # Add target="_blank" to all links
content = content.replace('<a href="', '<a target="_blank" href="') html = html.replace('<a href="', '<a target="_blank" href="')
html = html.replace("<h4", "<h4 style='margin-bottom:0px;'")
html = fix_numbered_lists(html)
return html
content = content.replace("<h4", "<h4 style='margin-bottom:0px;'")
content = fix_numbered_lists(content) def render_page(date, handshake_scripts=None):
# Get cached content
content = get_blog_content(date)
if content is None:
return render_template("404.html"), 404
# Get the title from the file name
title = date.removesuffix(".md").replace("_", " ")
# Convert the md to html (cached)
html_content = render_markdown_to_html(content)
return render_template( return render_template(
"blog/template.html", "blog/template.html",
title=title, title=title,
content=content, content=html_content,
handshake_scripts=handshake_scripts, handshake_scripts=handshake_scripts,
) )
@@ -134,12 +151,11 @@ def path(path):
if not isCLI(request): if not isCLI(request):
return render_page(path, handshake_scripts=getHandshakeScript(request.host)) return render_page(path, handshake_scripts=getHandshakeScript(request.host))
# Convert md to html # Get cached content
if not os.path.exists(f"data/blog/{path}.md"): content = get_blog_content(path)
if content is None:
return render_template("404.html"), 404 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 # Get the title from the file name
title = path.replace("_", " ") title = path.replace("_", " ")
return jsonify({ return jsonify({
@@ -154,11 +170,9 @@ def path(path):
@app.route("/<path:path>.md") @app.route("/<path:path>.md")
def path_md(path): def path_md(path):
if not os.path.exists(f"data/blog/{path}.md"): content = get_blog_content(path)
if content is None:
return render_template("404.html"), 404 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 the raw markdown file
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'} return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}

View File

@@ -1,6 +1,7 @@
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 functools import lru_cache
from tools import getHandshakeScript, error_response, isCLI from tools import getHandshakeScript, error_response, isCLI
from curl import get_header, MAX_WIDTH from curl import get_header, MAX_WIDTH
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@@ -10,6 +11,7 @@ import re
app = Blueprint('now', __name__, url_prefix='/now') app = Blueprint('now', __name__, url_prefix='/now')
@lru_cache(maxsize=16)
def list_page_files(): def list_page_files():
now_pages = os.listdir("templates/now") now_pages = os.listdir("templates/now")
now_pages = [ now_pages = [
@@ -19,12 +21,14 @@ def list_page_files():
return now_pages return now_pages
@lru_cache(maxsize=16)
def list_dates(): def list_dates():
now_pages = list_page_files() 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
@lru_cache(maxsize=8)
def get_latest_date(formatted=False): def get_latest_date(formatted=False):
if formatted: if formatted:
date = list_dates()[0] date = list_dates()[0]

250
cache_helper.py Normal file
View File

@@ -0,0 +1,250 @@
"""
Cache helper module for expensive API calls and configuration.
Provides centralized caching with TTL for external API calls.
"""
import datetime
import os
import json
import requests
from functools import lru_cache
# Cache storage for NC_CONFIG with timestamp
_nc_config_cache = {"data": None, "timestamp": 0}
_nc_config_ttl = 3600 # 1 hour cache
def get_nc_config():
"""
Get NC_CONFIG with caching (1 hour TTL).
Falls back to default config on error.
Returns:
dict: Configuration dictionary
"""
global _nc_config_cache
current_time = datetime.datetime.now().timestamp()
# Check if cache is valid
if _nc_config_cache["data"] and (current_time - _nc_config_cache["timestamp"]) < _nc_config_ttl:
return _nc_config_cache["data"]
# Fetch new config
try:
config = requests.get(
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json",
timeout=5
).json()
_nc_config_cache = {"data": config, "timestamp": current_time}
return config
except Exception as e:
print(f"Error fetching NC_CONFIG: {e}")
# Return cached data if available, otherwise default
if _nc_config_cache["data"]:
return _nc_config_cache["data"]
return {"time-zone": 10, "message": ""}
# Cache storage for git data
_git_data_cache = {"data": None, "timestamp": 0}
_git_data_ttl = 300 # 5 minutes cache
def get_git_latest_activity():
"""
Get latest git activity with caching (5 minute TTL).
Returns:
dict: Git activity data or default values
"""
global _git_data_cache
current_time = datetime.datetime.now().timestamp()
# Check if cache is valid
if _git_data_cache["data"] and (current_time - _git_data_cache["timestamp"]) < _git_data_ttl:
return _git_data_cache["data"]
# Fetch new data
try:
git = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
headers={"Authorization": os.getenv("GIT_AUTH") or os.getenv("git_token") or ""},
timeout=5
)
git_data = git.json()
if git_data and len(git_data) > 0:
result = git_data[0]
_git_data_cache = {"data": result, "timestamp": current_time}
return result
except Exception as e:
print(f"Error fetching git data: {e}")
# Return cached or default
if _git_data_cache["data"]:
return _git_data_cache["data"]
return {
"repo": {
"html_url": "https://nathan.woodburn.au",
"name": "nathanwoodburn.github.io",
"description": "Personal website",
}
}
# Cache storage for projects
_projects_cache = {"data": None, "timestamp": 0}
_projects_ttl = 7200 # 2 hours cache
def get_projects(limit=3):
"""
Get projects list with caching (2 hour TTL).
Args:
limit (int): Number of projects to return
Returns:
list: List of project dictionaries
"""
global _projects_cache
current_time = datetime.datetime.now().timestamp()
# Check if cache is valid
if _projects_cache["data"] and (current_time - _projects_cache["timestamp"]) < _projects_ttl:
return _projects_cache["data"][:limit]
# Fetch new data
try:
projects = []
projectsreq = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos",
timeout=5
)
projects = projectsreq.json()
# Check for pagination
pageNum = 2
while 'rel="next"' in projectsreq.headers.get("link", ""):
projectsreq = requests.get(
f"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page={pageNum}",
timeout=5
)
projects += projectsreq.json()
pageNum += 1
# Safety limit
if pageNum > 10:
break
# Process projects
for project in projects:
if project.get("avatar_url") in ("https://git.woodburn.au/", ""):
project["avatar_url"] = "/favicon.png"
project["name"] = project["name"].replace("_", " ").replace("-", " ")
# Sort by last updated
projects_sorted = sorted(projects, key=lambda x: x.get("updated_at", ""), reverse=True)
# Remove duplicates by name
seen_names = set()
unique_projects = []
for project in projects_sorted:
if project["name"] not in seen_names:
unique_projects.append(project)
seen_names.add(project["name"])
_projects_cache = {"data": unique_projects, "timestamp": current_time}
return unique_projects[:limit]
except Exception as e:
print(f"Error fetching projects: {e}")
if _projects_cache["data"]:
return _projects_cache["data"][:limit]
return []
# Cache storage for uptime status
_uptime_cache = {"data": None, "timestamp": 0}
_uptime_ttl = 300 # 5 minutes cache
def get_uptime_status():
"""
Get uptime status with caching (5 minute TTL).
Returns:
bool: True if services are up, False otherwise
"""
global _uptime_cache
current_time = datetime.datetime.now().timestamp()
# Check if cache is valid
if _uptime_cache["data"] is not None and (current_time - _uptime_cache["timestamp"]) < _uptime_ttl:
return _uptime_cache["data"]
# Fetch new data
try:
uptime = requests.get(
"https://uptime.woodburn.au/api/status-page/main/badge",
timeout=5
)
content = uptime.content.decode("utf-8").lower()
status = "maintenance" in content or uptime.content.count(b"Up") > 1
_uptime_cache = {"data": status, "timestamp": current_time}
return status
except Exception as e:
print(f"Error fetching uptime: {e}")
# Return cached or default (assume up)
if _uptime_cache["data"] is not None:
return _uptime_cache["data"]
return True
# Cached wallet data loaders
@lru_cache(maxsize=1)
def get_wallet_tokens():
"""
Get wallet tokens with caching.
Returns:
list: List of token dictionaries
"""
try:
with open(".well-known/wallets/.tokens") as file:
return json.load(file)
except Exception as e:
print(f"Error loading tokens: {e}")
return []
@lru_cache(maxsize=1)
def get_coin_names():
"""
Get coin names with caching.
Returns:
dict: Dictionary of coin names
"""
try:
with open(".well-known/wallets/.coins") as file:
return json.load(file)
except Exception as e:
print(f"Error loading coin names: {e}")
return {}
@lru_cache(maxsize=1)
def get_wallet_domains():
"""
Get wallet domains with caching.
Returns:
dict: Dictionary of wallet domains
"""
try:
if os.path.isfile(".well-known/wallets/.domains"):
with open(".well-known/wallets/.domains") as file:
return json.load(file)
except Exception as e:
print(f"Error loading domains: {e}")
return {}

55
curl.py
View File

@@ -2,8 +2,8 @@ from flask import render_template
from tools import getAddress, get_tools_data, getClientIP from tools import getAddress, get_tools_data, getClientIP
import os import os
from functools import lru_cache from functools import lru_cache
import requests
from blueprints.spotify import get_spotify_track from blueprints.spotify import get_spotify_track
from cache_helper import get_git_latest_activity, get_projects as get_projects_cached
MAX_WIDTH = 80 MAX_WIDTH = 80
@@ -24,61 +24,26 @@ def get_header():
with open("templates/header.ascii", "r") as f: with open("templates/header.ascii", "r") as f:
return f.read() return f.read()
@lru_cache(maxsize=1) @lru_cache(maxsize=16)
def get_current_project(): def get_current_project():
git = requests.get( git = get_git_latest_activity()
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1", repo_name = git["repo"]["name"].lower()
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"] repo_description = git["repo"]["description"]
if not repo_description: if not repo_description:
return f"{repo_name}" return f"[1;36m{repo_name}[0m"
return f"{repo_name} - {repo_description}" return f"[1;36m{repo_name}[0m - [1m{repo_description}[0m"
@lru_cache(maxsize=1) @lru_cache(maxsize=16)
def get_projects(): def get_projects():
projectsreq = requests.get( projects_data = get_projects_cached(limit=5)
"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 = "" projects = ""
projectNum = 0 for project in projects_data:
includedNames = [] projects += f"""[1m{project['name']}[0m - {project['description'] if project['description'] else 'No description'}
while len(includedNames) < 5 and projectNum < len(projectsList):
# Avoid duplicates
if projectsList[projectNum]["name"] in includedNames:
projectNum += 1
continue
includedNames.append(projectsList[projectNum]["name"])
project = projectsList[projectNum]
projects += f"""{project['name']} - {project['description'] if project['description'] else 'No description'}
{project['html_url']} {project['html_url']}
""" """
projectNum += 1
return projects return projects
def curl_response(request): def curl_response(request):
# Check if <path>.ascii exists # Check if <path>.ascii exists
path = clean_path(request.path) path = clean_path(request.path)

150
server.py
View File

@@ -33,6 +33,15 @@ from tools import (
get_tools_data, get_tools_data,
) )
from curl import curl_response from curl import curl_response
from cache_helper import (
get_nc_config,
get_git_latest_activity,
get_projects,
get_uptime_status,
get_wallet_tokens,
get_coin_names,
get_wallet_domains,
)
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
@@ -70,13 +79,6 @@ if os.path.isfile("data/sites.json"):
# Remove any sites that are not enabled # Remove any sites that are not enabled
SITES = [site for site in SITES if "enabled" not in site or site["enabled"]] SITES = [site for site in SITES if "enabled" not in site or site["enabled"]]
PROJECTS = []
PROJECTS_UPDATED = 0
NC_CONFIG = requests.get(
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json"
).json()
# endregion # endregion
# region Assets routes # region Assets routes
@@ -226,9 +228,6 @@ def api_legacy(function):
@app.route("/") @app.route("/")
def index(): def index():
global PROJECTS
global PROJECTS_UPDATED
# Check if host if podcast.woodburn.au # Check if host if podcast.woodburn.au
if "podcast.woodburn.au" in request.host: if "podcast.woodburn.au" in request.host:
return render_template("podcast.html") return render_template("podcast.html")
@@ -259,81 +258,22 @@ def index():
resp.set_cookie("loaded", "true", max_age=604800) resp.set_cookie("loaded", "true", max_age=604800)
return resp return resp
try: # Use cached git data
git = requests.get( git = get_git_latest_activity()
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1", repo_name = git["repo"]["name"].lower()
headers={"Authorization": os.getenv("GIT_AUTH")},
)
git = git.json()
git = git[0]
repo_name = git["repo"]["name"]
repo_name = repo_name.lower()
repo_description = git["repo"]["description"] repo_description = git["repo"]["description"]
except Exception as e:
repo_name = "nathanwoodburn.github.io"
repo_description = "Personal website"
git = {
"repo": {
"html_url": "https://nathan.woodburn.au",
"name": "nathanwoodburn.github.io",
"description": "Personal website",
}
}
print(f"Error getting git data: {e}")
# Get only repo names for the newest updates # Use cached projects data
if ( projects = get_projects(limit=3)
PROJECTS == []
or PROJECTS_UPDATED
< (datetime.datetime.now() - datetime.timedelta(hours=2)).timestamp()
):
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
for project in PROJECTS:
if (
project["avatar_url"] == "https://git.woodburn.au/"
or project["avatar_url"] == ""
):
project["avatar_url"] = "/favicon.png"
project["name"] = project["name"].replace("_", " ").replace("-", " ")
# Sort by last updated
projectsList = sorted(PROJECTS, key=lambda x: x["updated_at"], reverse=True)
PROJECTS = []
projectNames = []
projectNum = 0
while len(PROJECTS) < 3:
if projectsList[projectNum]["name"] not in projectNames:
PROJECTS.append(projectsList[projectNum])
projectNames.append(projectsList[projectNum]["name"])
projectNum += 1
PROJECTS_UPDATED = datetime.datetime.now().timestamp()
# Use cached uptime status
uptime = get_uptime_status()
custom = "" custom = ""
# Check for downtime
uptime = requests.get("https://uptime.woodburn.au/api/status-page/main/badge")
if "maintenance" in uptime.content.decode("utf-8").lower():
uptime = True
else:
uptime = uptime.content.count(b"Up") > 1
if uptime: if uptime:
custom += "<style>#downtime{display:none !important;}</style>" custom += "<style>#downtime{display:none !important;}</style>"
else: else:
custom += "<style>#downtime{opacity:1;}</style>" custom += "<style>#downtime{opacity:1;}</style>"
# Special names # Special names
if repo_name == "nathanwoodburn.github.io": if repo_name == "nathanwoodburn.github.io":
repo_name = "Nathan.Woodburn/" repo_name = "Nathan.Woodburn/"
@@ -341,8 +281,9 @@ def index():
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>"
# Get time # Get time using cached config
timezone_offset = datetime.timedelta(hours=NC_CONFIG["time-zone"]) nc_config = get_nc_config()
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) time = datetime.datetime.now(tz=timezone)
@@ -365,7 +306,7 @@ def index():
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
} }
""" """
time += f"startClock({NC_CONFIG['time-zone']});" time += f"startClock({nc_config['time-zone']});"
time += "</script>" time += "</script>"
HNSaddress = getAddress("HNS") HNSaddress = getAddress("HNS")
@@ -385,9 +326,9 @@ def index():
repo_description=repo_description, repo_description=repo_description,
custom=custom, custom=custom,
sites=SITES, sites=SITES,
projects=PROJECTS, projects=projects,
time=time, time=time,
message=NC_CONFIG.get("message", ""), message=nc_config.get("message", ""),
), ),
200, 200,
{"Content-Type": "text/html"}, {"Content-Type": "text/html"},
@@ -409,31 +350,21 @@ def donate():
coinList = [file for file in coinList if file[0] != "."] coinList = [file for file in coinList if file[0] != "."]
coinList.sort() coinList.sort()
tokenList = [] tokenList = get_wallet_tokens()
coinNames = get_coin_names()
with open(".well-known/wallets/.tokens") as file:
tokenList = file.read()
tokenList = json.loads(tokenList)
coinNames = {}
with open(".well-known/wallets/.coins") as file:
coinNames = file.read()
coinNames = json.loads(coinNames)
coins = "" coins = ""
default_coins = ["btc", "eth", "hns", "sol", "xrp", "ada", "dot"] default_coins = ["btc", "eth", "hns", "sol", "xrp", "ada", "dot"]
for file in coinList: for file in coinList:
if file in coinNames: coin_name = coinNames.get(file, file)
coins += f'<a class="dropdown-item" style="{"display:none;" if file.lower() not in default_coins else ""}" href="?c={file.lower()}">{coinNames[file]}</a>' display_style = "" if file.lower() in default_coins else "display:none;"
else: coins += f'<a class="dropdown-item" style="{display_style}" href="?c={file.lower()}">{coin_name}</a>'
coins += f'<a class="dropdown-item" style="{"display:none;" if file.lower() not in default_coins else ""}" href="?c={file.lower()}">{file}</a>'
for token in tokenList: for token in tokenList:
if token["chain"] != "null": chain_display = f" on {token['chain']}" if token["chain"] != "null" else ""
coins += f'<a class="dropdown-item" style="display:none;" href="?t={token["symbol"].lower()}&c={token["chain"].lower()}">{token["name"]} ({token["symbol"] + " on " if token["symbol"] != token["name"] else ""}{token["chain"]})</a>' symbol_display = f" ({token['symbol']}{chain_display})" if token["symbol"] != token["name"] else chain_display
else: coins += f'<a class="dropdown-item" style="display:none;" href="?t={token["symbol"].lower()}&c={token["chain"].lower()}">{token["name"]}{symbol_display}</a>'
coins += f'<a class="dropdown-item" style="display:none;" href="?t={token["symbol"].lower()}&c={token["chain"].lower()}">{token["name"]} ({token["symbol"] if token["symbol"] != token["name"] else ""})</a>'
crypto = request.args.get("c") crypto = request.args.get("c")
if not crypto: if not crypto:
@@ -460,7 +391,6 @@ def donate():
token = {"name": "Unknown token", "symbol": token, "chain": crypto} token = {"name": "Unknown token", "symbol": token, "chain": crypto}
address = "" address = ""
domain = ""
cryptoHTML = "" cryptoHTML = ""
proof = "" proof = ""
@@ -470,10 +400,12 @@ def donate():
if os.path.isfile(f".well-known/wallets/{crypto}"): if os.path.isfile(f".well-known/wallets/{crypto}"):
with open(f".well-known/wallets/{crypto}") as file: with open(f".well-known/wallets/{crypto}") as file:
address = file.read() address = file.read()
coin_display = coinNames.get(crypto, crypto)
if not token: if not token:
cryptoHTML += f"<br>Donate with {coinNames[crypto] if crypto in coinNames else crypto}:" cryptoHTML += f"<br>Donate with {coin_display}:"
else: else:
cryptoHTML += f"<br>Donate with {token['name']} {'(' + token['symbol'] + ') ' if token['symbol'] != token['name'] else ''}on {crypto}:" token_symbol = f" ({token['symbol']})" if token['symbol'] != token['name'] else ""
cryptoHTML += f"<br>Donate with {token['name']}{token_symbol} on {crypto}:"
cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>' cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>'
if proof: if proof:
@@ -481,7 +413,9 @@ def donate():
elif token: elif token:
if "address" in token: if "address" in token:
address = token["address"] address = token["address"]
cryptoHTML += f"<br>Donate with {token['name']} {'(' + token['symbol'] + ')' if token['symbol'] != token['name'] else ''}{' on ' + crypto if crypto != 'NULL' else ''}:" token_symbol = f" ({token['symbol']})" if token['symbol'] != token['name'] else ""
chain_display = f" on {crypto}" if crypto != 'NULL' else ""
cryptoHTML += f"<br>Donate with {token['name']}{token_symbol}{chain_display}:"
cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>' cryptoHTML += f'<br><code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-address" class="address" style="color: rgb(242,90,5);display: inline-block;" data-bs-original-title="Click to copy">{address}</code>'
if proof: if proof:
cryptoHTML += proof cryptoHTML += proof
@@ -490,16 +424,12 @@ def donate():
else: else:
cryptoHTML += f"<br>Invalid chain: {crypto}<br>" cryptoHTML += f"<br>Invalid chain: {crypto}<br>"
if os.path.isfile(".well-known/wallets/.domains"): domains = get_wallet_domains()
# Get json of all domains
with open(".well-known/wallets/.domains") as file:
domains = file.read()
domains = json.loads(domains)
if crypto in domains: if crypto in domains:
domain = domains[crypto] domain = domains[crypto]
cryptoHTML += "<br>Or send to this domain on compatible wallets:<br>" cryptoHTML += "<br>Or send to this domain on compatible wallets:<br>"
cryptoHTML += f'<code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-domain" class="address" style="color: rgb(242,90,5);display: block;" data-bs-original-title="Click to copy">{domain}</code>' cryptoHTML += f'<code data-bs-toggle="tooltip" data-bss-tooltip="" id="crypto-domain" class="address" style="color: rgb(242,90,5);display: block;" data-bs-original-title="Click to copy">{domain}</code>'
if address: if address:
cryptoHTML += ( cryptoHTML += (
'<br><img src="/address/' '<br><img src="/address/'

View File

@@ -1,6 +1,6 @@
from flask import Request, render_template, jsonify, make_response from flask import Request, render_template, jsonify, make_response
import os import os
from functools import lru_cache as cache from functools import lru_cache
import datetime import datetime
from typing import Optional, Dict, Union, Tuple from typing import Optional, Dict, Union, Tuple
import re import re
@@ -56,7 +56,7 @@ def getClientIP(request: Request) -> str:
ip = "unknown" ip = "unknown"
return ip return ip
@cache @lru_cache(maxsize=1)
def getGitCommit() -> str: def getGitCommit() -> str:
""" """
Get the current git commit hash. Get the current git commit hash.
@@ -115,7 +115,7 @@ def isCrawler(request: Request) -> bool:
return any(crawler in user_agent for crawler in CRAWLERS) return any(crawler in user_agent for crawler in CRAWLERS)
return False return False
@cache @lru_cache(maxsize=128)
def isDev(host: str) -> bool: def isDev(host: str) -> bool:
""" """
Check if the host indicates a development environment. Check if the host indicates a development environment.
@@ -135,7 +135,7 @@ def isDev(host: str) -> bool:
return True return True
return False return False
@cache @lru_cache(maxsize=128)
def getHandshakeScript(host: str) -> str: def getHandshakeScript(host: str) -> str:
""" """
Get the handshake script HTML snippet. Get the handshake script HTML snippet.
@@ -150,7 +150,7 @@ def getHandshakeScript(host: str) -> str:
return "" return ""
return '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>' return '<script src="https://nathan.woodburn/handshake.js" domain="nathan.woodburn" async></script><script src="https://nathan.woodburn/https.js" async></script>'
@cache @lru_cache(maxsize=64)
def getAddress(coin: str) -> str: def getAddress(coin: str) -> str:
""" """
Get the wallet address for a cryptocurrency. Get the wallet address for a cryptocurrency.
@@ -169,7 +169,7 @@ def getAddress(coin: str) -> str:
return address return address
@cache @lru_cache(maxsize=256)
def getFilePath(name: str, path: str) -> Optional[str]: def getFilePath(name: str, path: str) -> Optional[str]:
""" """
Find a file in a directory tree. Find a file in a directory tree.