2 Commits

Author SHA1 Message Date
e489764ff8 fix: Add escape char for curl rendering and format python files
All checks were successful
Build Docker / BuildImage (push) Successful in 1m6s
Check Code Quality / RuffCheck (push) Successful in 1m20s
2025-11-21 23:05:40 +11:00
51c4416d4d fix: Add cahce helper to dockerfile
All checks were successful
Build Docker / BuildImage (push) Successful in 1m4s
Check Code Quality / RuffCheck (push) Successful in 1m6s
2025-11-21 22:55:02 +11:00
18 changed files with 463 additions and 310 deletions

View File

@@ -20,7 +20,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# Copy only app source files # Copy only app source files
COPY blueprints blueprints COPY blueprints blueprints
COPY main.py server.py curl.py tools.py mail.py ./ COPY main.py server.py curl.py tools.py mail.py cache_helper.py ./
COPY templates templates COPY templates templates
COPY data data COPY data data
COPY pwa pwa COPY pwa pwa
@@ -55,6 +55,7 @@ COPY --from=build --chown=appuser:appgroup /app/server.py /app/
COPY --from=build --chown=appuser:appgroup /app/curl.py /app/ COPY --from=build --chown=appuser:appgroup /app/curl.py /app/
COPY --from=build --chown=appuser:appgroup /app/tools.py /app/ COPY --from=build --chown=appuser:appgroup /app/tools.py /app/
COPY --from=build --chown=appuser:appgroup /app/mail.py /app/ COPY --from=build --chown=appuser:appgroup /app/mail.py /app/
COPY --from=build --chown=appuser:appgroup /app/cache_helper.py /app/
USER appuser USER appuser
EXPOSE 5000 EXPOSE 5000

View File

@@ -1,35 +1,38 @@
import os import os
import json import json
if not os.path.exists('.well-known/wallets'): if not os.path.exists(".well-known/wallets"):
os.makedirs('.well-known/wallets') os.makedirs(".well-known/wallets")
def addCoin(token:str, name:str, address:str):
with open('.well-known/wallets/'+token.upper(),'w') as f: def addCoin(token: str, name: str, address: str):
with open(".well-known/wallets/" + token.upper(), "w") as f:
f.write(address) f.write(address)
with open('.well-known/wallets/.coins','r') as f: with open(".well-known/wallets/.coins", "r") as f:
coins = json.load(f) coins = json.load(f)
coins[token.upper()] = f'{name} ({token.upper()})' coins[token.upper()] = f"{name} ({token.upper()})"
with open('.well-known/wallets/.coins','w') as f: with open(".well-known/wallets/.coins", "w") as f:
f.write(json.dumps(coins, indent=4)) f.write(json.dumps(coins, indent=4))
def addDomain(token:str, domain:str):
with open('.well-known/wallets/.domains','r') as f: def addDomain(token: str, domain: str):
with open(".well-known/wallets/.domains", "r") as f:
domains = json.load(f) domains = json.load(f)
domains[token.upper()] = domain domains[token.upper()] = domain
with open('.well-known/wallets/.domains','w') as f: with open(".well-known/wallets/.domains", "w") as f:
f.write(json.dumps(domains, indent=4)) f.write(json.dumps(domains, indent=4))
if __name__ == '__main__':
if __name__ == "__main__":
# Ask user for token # Ask user for token
token = input('Enter token symbol: ') token = input("Enter token symbol: ")
name = input('Enter token name: ') name = input("Enter token name: ")
address = input('Enter wallet address: ') address = input("Enter wallet address: ")
addCoin(token, name, address) addCoin(token, name, address)
if input('Do you want to add a domain? (y/n): ').lower() == 'y': if input("Do you want to add a domain? (y/n): ").lower() == "y":
domain = input('Enter domain: ') domain = input("Enter domain: ")
addDomain(token, domain) addDomain(token, domain)

View File

@@ -3,7 +3,7 @@ import os
from cloudflare import Cloudflare from cloudflare import Cloudflare
from tools import json_response from tools import json_response
app = Blueprint('acme', __name__) app = Blueprint("acme", __name__)
@app.route("/hnsdoh-acme", methods=["POST"]) @app.route("/hnsdoh-acme", methods=["POST"])
@@ -23,7 +23,9 @@ def post():
zone = cf.zones.list(name="hnsdoh.com").to_dict() zone = cf.zones.list(name="hnsdoh.com").to_dict()
zone_id = zone["result"][0]["id"] # type: ignore zone_id = zone["result"][0]["id"] # type: ignore
existing_records = cf.dns.records.list( existing_records = cf.dns.records.list(
zone_id=zone_id, type="TXT", name="_acme-challenge.hnsdoh.com" # type: ignore zone_id=zone_id,
type="TXT",
name="_acme-challenge.hnsdoh.com", # type: ignore
).to_dict() ).to_dict()
record_id = existing_records["result"][0]["id"] # type: ignore 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.delete(dns_record_id=record_id, zone_id=zone_id)

View File

@@ -18,7 +18,7 @@ HTTP_NOT_FOUND = 404
HTTP_UNSUPPORTED_MEDIA = 415 HTTP_UNSUPPORTED_MEDIA = 415
HTTP_SERVER_ERROR = 500 HTTP_SERVER_ERROR = 500
app = Blueprint('api', __name__, url_prefix='/api/v1') app = Blueprint("api", __name__, url_prefix="/api/v1")
# Register solana blueprint # Register solana blueprint
app.register_blueprint(sol.app) app.register_blueprint(sol.app)
@@ -27,7 +27,8 @@ app.register_blueprint(sol.app)
@app.route("/help") @app.route("/help")
def help(): def help():
"""Provide API documentation and help.""" """Provide API documentation and help."""
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",
@@ -42,19 +43,22 @@ def help():
"/ping": "Just check if the site is up", "/ping": "Just check if the site is up",
"/ip": "Get your IP address", "/ip": "Get your IP address",
"/headers": "Get your request headers", "/headers": "Get your request headers",
"/help": "Get this help message" "/help": "Get this help message",
}, },
"base_url": "/api/v1", "base_url": "/api/v1",
"version": getGitCommit(), "version": getGitCommit(),
"ip": getClientIP(request), "ip": getClientIP(request),
"status": HTTP_OK "status": HTTP_OK,
}) }
)
@app.route("/status") @app.route("/status")
@app.route("/ping") @app.route("/ping")
def status(): def status():
return json_response(request, "200 OK", HTTP_OK) return json_response(request, "200 OK", HTTP_OK)
@app.route("/version") @app.route("/version")
def version(): def version():
"""Get the current version of the website.""" """Get the current version of the website."""
@@ -68,45 +72,44 @@ def time():
timezone_offset = datetime.timedelta(hours=nc_config["time-zone"]) 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,
}) }
)
@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() 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,
}) }
)
@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() nc_config = get_nc_config()
return jsonify({ return jsonify(
"message": nc_config["message"], {"message": nc_config["message"], "ip": getClientIP(request), "status": HTTP_OK}
"ip": getClientIP(request), )
"status": HTTP_OK
})
@app.route("/ip") @app.route("/ip")
def ip(): def ip():
"""Get the client's IP address.""" """Get the client's IP address."""
return jsonify({ return jsonify({"ip": getClientIP(request), "status": HTTP_OK})
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/email", methods=["POST"]) @app.route("/email", methods=["POST"])
@@ -114,7 +117,9 @@ def email_post():
"""Send an email via the API (requires API key).""" """Send an email via the API (requires API key)."""
# Verify json # Verify json
if not request.is_json: if not request.is_json:
return json_response(request, "415 Unsupported Media Type", HTTP_UNSUPPORTED_MEDIA) return json_response(
request, "415 Unsupported Media Type", HTTP_UNSUPPORTED_MEDIA
)
# Check if api key sent # Check if api key sent
data = request.json data = request.json
@@ -145,13 +150,16 @@ def project():
"website": git["repo"].get("website"), "website": git["repo"].get("website"),
} }
return jsonify({ return jsonify(
{
"repo_name": repo_name, "repo_name": repo_name,
"repo_description": repo_description, "repo_description": repo_description,
"repo": gitinfo, "repo": gitinfo,
"ip": getClientIP(request), "ip": getClientIP(request),
"status": HTTP_OK "status": HTTP_OK,
}) }
)
@app.route("/tools") @app.route("/tools")
def tools(): def tools():
@@ -164,6 +172,7 @@ def tools():
return json_response(request, {"tools": tools}, HTTP_OK) return json_response(request, {"tools": tools}, HTTP_OK)
@app.route("/playing") @app.route("/playing")
def playing(): def playing():
"""Get the currently playing Spotify track.""" """Get the currently playing Spotify track."""
@@ -186,15 +195,11 @@ def headers():
# Remove from headers # Remove from headers
toremove.append(key) toremove.append(key)
for key in toremove: for key in toremove:
headers.pop(key) headers.pop(key)
return jsonify({ return jsonify({"headers": headers, "ip": getClientIP(request), "status": HTTP_OK})
"headers": headers,
"ip": getClientIP(request),
"status": HTTP_OK
})
@app.route("/page_date") @app.route("/page_date")
def page_date(): def page_date():
@@ -211,33 +216,33 @@ def page_date():
r = requests.get(url, timeout=5) r = requests.get(url, timeout=5)
r.raise_for_status() r.raise_for_status()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
return json_response(request, f"400 Bad Request 'url' unreachable: {e}", HTTP_BAD_REQUEST) return json_response(
request, f"400 Bad Request 'url' unreachable: {e}", HTTP_BAD_REQUEST
)
page_text = r.text page_text = r.text
# Remove ordinal suffixes globally # Remove ordinal suffixes globally
page_text = re.sub(r'(\d+)(st|nd|rd|th)', r'\1', page_text, flags=re.IGNORECASE) page_text = re.sub(r"(\d+)(st|nd|rd|th)", r"\1", page_text, flags=re.IGNORECASE)
# Remove HTML comments # Remove HTML comments
page_text = re.sub(r'<!--.*?-->', '', page_text, flags=re.DOTALL) page_text = re.sub(r"<!--.*?-->", "", page_text, flags=re.DOTALL)
date_patterns = [ date_patterns = [
r'(\d{4})[/-](\d{1,2})[/-](\d{1,2})', # YYYY-MM-DD 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"(\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"(?: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\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"\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 r"(?:Last updated:|Updated:|Last update)?\s*([A-Za-z]{3,9})\s+(\d{4})", # Month YYYY only
] ]
# Structured data patterns # Structured data patterns
json_date_patterns = { json_date_patterns = {
r'"datePublished"\s*:\s*"([^"]+)"': "published", r'"datePublished"\s*:\s*"([^"]+)"': "published",
r'"dateModified"\s*:\s*"([^"]+)"': "modified", 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:published_time"\s+content\s*=\s*"([^"]+)"': "published",
r'<meta\s+(?:[^>]*?)property\s*=\s*"article:modified_time"\s+content\s*=\s*"([^"]+)"': "modified", r'<meta\s+(?:[^>]*?)property\s*=\s*"article:modified_time"\s+content\s*=\s*"([^"]+)"': "modified",
r'<time\s+datetime\s*=\s*"([^"]+)"': "published" r'<time\s+datetime\s*=\s*"([^"]+)"': "published",
} }
found_dates = [] found_dates = []
@@ -255,7 +260,7 @@ def page_date():
for match in re.findall(pattern, page_text): for match in re.findall(pattern, page_text):
try: try:
dt = date_parser.isoparse(match) dt = date_parser.isoparse(match)
formatted_date = dt.strftime('%Y-%m-%d') formatted_date = dt.strftime("%Y-%m-%d")
found_dates.append([[formatted_date], -1, date_type]) found_dates.append([[formatted_date], -1, date_type])
except (ValueError, TypeError): except (ValueError, TypeError):
continue continue
@@ -264,7 +269,9 @@ def page_date():
return json_response(request, "Date not found on page", HTTP_BAD_REQUEST) return json_response(request, "Date not found on page", HTTP_BAD_REQUEST)
today = datetime.date.today() today = datetime.date.today()
tolerance_date = today + datetime.timedelta(days=1) # Allow for slight future dates (e.g., time zones) tolerance_date = today + datetime.timedelta(
days=1
) # Allow for slight future dates (e.g., time zones)
# When processing dates # When processing dates
processed_dates = [] processed_dates = []
for date_groups, pattern_format, date_type in found_dates: for date_groups, pattern_format, date_type in found_dates:
@@ -285,18 +292,32 @@ def page_date():
date_obj = {"date": dt.strftime("%Y-%m-%d"), "type": date_type} date_obj = {"date": dt.strftime("%Y-%m-%d"), "type": date_type}
if verbose: if verbose:
if pattern_format == -1: if pattern_format == -1:
date_obj.update({"source": "metadata", "pattern_used": pattern_format, "raw": date_groups[0]}) date_obj.update(
{
"source": "metadata",
"pattern_used": pattern_format,
"raw": date_groups[0],
}
)
else: else:
date_obj.update({"source": "content", "pattern_used": pattern_format, "raw": " ".join(date_groups)}) date_obj.update(
{
"source": "content",
"pattern_used": pattern_format,
"raw": " ".join(date_groups),
}
)
processed_dates.append(date_obj) processed_dates.append(date_obj)
if not processed_dates: if not processed_dates:
if verbose: if verbose:
return jsonify({ return jsonify(
{
"message": "No valid dates found on page", "message": "No valid dates found on page",
"found_dates": found_dates, "found_dates": found_dates,
"processed_dates": processed_dates "processed_dates": processed_dates,
}), HTTP_BAD_REQUEST }
), HTTP_BAD_REQUEST
return json_response(request, "No valid dates found on page", HTTP_BAD_REQUEST) return json_response(request, "No valid dates found on page", HTTP_BAD_REQUEST)
# Sort dates and return latest # Sort dates and return latest
processed_dates.sort(key=lambda x: x["date"]) processed_dates.sort(key=lambda x: x["date"])

View File

@@ -6,7 +6,7 @@ import re
from functools import lru_cache 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) @lru_cache(maxsize=32)
@@ -14,11 +14,13 @@ 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
blog_pages.sort( blog_pages.sort(
key=lambda x: os.path.getmtime(os.path.join("data/blog", x)), reverse=True) 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 = [
for page in blog_pages if page.endswith(".md")] page.removesuffix(".md") for page in blog_pages if page.endswith(".md")
]
return blog_pages return blog_pages
@@ -37,7 +39,8 @@ def get_blog_content(date):
def render_markdown_to_html(content): def render_markdown_to_html(content):
"""Convert markdown to HTML with caching.""" """Convert markdown to HTML with caching."""
html = markdown.markdown( 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
html = html.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 = html.replace("<h4", "<h4 style='margin-bottom:0px;'")
@@ -65,18 +68,18 @@ def render_page(date, handshake_scripts=None):
def fix_numbered_lists(html): def fix_numbered_lists(html):
soup = BeautifulSoup(html, 'html.parser') soup = BeautifulSoup(html, "html.parser")
# Find the <p> tag containing numbered steps # Find the <p> tag containing numbered steps
paragraphs = soup.find_all('p') paragraphs = soup.find_all("p")
for p in paragraphs: for p in paragraphs:
content = p.decode_contents() # type: ignore content = p.decode_contents() # type: ignore
# Check for likely numbered step structure # Check for likely numbered step structure
if re.search(r'1\.\s', content): if re.search(r"1\.\s", content):
# Split into pre-list and numbered steps # Split into pre-list and numbered steps
# Match: <br>, optional whitespace, then a number and dot # Match: <br>, optional whitespace, then a number and dot
parts = re.split(r'(?:<br\s*/?>)?\s*(\d+)\.\s', content) parts = re.split(r"(?:<br\s*/?>)?\s*(\d+)\.\s", content)
# Result: [pre-text, '1', step1, '2', step2, ..., '10', step10] # Result: [pre-text, '1', step1, '2', step2, ..., '10', step10]
pre_text = parts[0].strip() pre_text = parts[0].strip()
@@ -85,10 +88,9 @@ def fix_numbered_lists(html):
# Assemble the ordered list # Assemble the ordered list
ol_items = [] ol_items = []
for i in range(0, len(steps), 2): for i in range(0, len(steps), 2):
if i+1 < len(steps): if i + 1 < len(steps):
step_html = steps[i+1].strip() step_html = steps[i + 1].strip()
ol_items.append( ol_items.append(f"<li style='list-style: auto;'>{step_html}</li>")
f"<li style='list-style: auto;'>{step_html}</li>")
# Build the final list HTML # Build the final list HTML
ol_html = "<ol>\n" + "\n".join(ol_items) + "\n</ol>" ol_html = "<ol>\n" + "\n".join(ol_items) + "\n</ol>"
@@ -97,7 +99,7 @@ def fix_numbered_lists(html):
new_html = f"{pre_text}<br />\n{ol_html}" if pre_text else ol_html new_html = f"{pre_text}<br />\n{ol_html}" if pre_text else ol_html
# Replace old <p> with parsed version # Replace old <p> with parsed version
new_fragment = BeautifulSoup(new_html, 'html.parser') new_fragment = BeautifulSoup(new_html, "html.parser")
p.replace_with(new_fragment) p.replace_with(new_fragment)
break # Only process the first matching <p> break # Only process the first matching <p>
@@ -134,16 +136,23 @@ def index():
blog_pages = list_page_files() blog_pages = list_page_files()
# Create a html list of pages # Create a html list of pages
blog_pages = [ blog_pages = [
{"name": page.replace("_", " "), "url": f"/blog/{page}", "download": f"/blog/{page}.md"} for page in blog_pages {
"name": page.replace("_", " "),
"url": f"/blog/{page}",
"download": f"/blog/{page}.md",
}
for page in blog_pages
] ]
# Render the template # Render the template
return jsonify({ return jsonify(
{
"status": 200, "status": 200,
"message": "Check out my various blog postsa", "message": "Check out my various blog postsa",
"ip": getClientIP(request), "ip": getClientIP(request),
"blogs": blog_pages "blogs": blog_pages,
}), 200 }
), 200
@app.route("/<path:path>") @app.route("/<path:path>")
@@ -158,14 +167,16 @@ def path(path):
# Get the title from the file name # Get the title from the file name
title = path.replace("_", " ") title = path.replace("_", " ")
return jsonify({ return jsonify(
{
"status": 200, "status": 200,
"message": f"Blog post: {title}", "message": f"Blog post: {title}",
"ip": getClientIP(request), "ip": getClientIP(request),
"title": title, "title": title,
"content": content, "content": content,
"download": f"/blog/{path}.md" "download": f"/blog/{path}.md",
}), 200 }
), 200
@app.route("/<path:path>.md") @app.route("/<path:path>.md")
@@ -175,4 +186,4 @@ def path_md(path):
return render_template("404.html"), 404 return render_template("404.html"), 404
# 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

@@ -8,7 +8,7 @@ from bs4 import BeautifulSoup
import re import re
# Create blueprint # Create blueprint
app = Blueprint('now', __name__, url_prefix='/now') app = Blueprint("now", __name__, url_prefix="/now")
@lru_cache(maxsize=16) @lru_cache(maxsize=16)
@@ -55,7 +55,10 @@ def render(date, handshake_scripts=None):
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d") date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
date_formatted = date_formatted.strftime("%A, %B %d, %Y") date_formatted = date_formatted.strftime("%A, %B %d, %Y")
return render_template(f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts) return render_template(
f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts
)
def render_curl(date=None): def render_curl(date=None):
# If the date is not available, render the latest page # If the date is not available, render the latest page
@@ -75,7 +78,7 @@ def render_curl(date=None):
# Load HTML # Load HTML
with open(f"templates/now/{date}.html", "r", encoding="utf-8") as f: with open(f"templates/now/{date}.html", "r", encoding="utf-8") as f:
raw_html = f.read().replace("{{ date }}", date_formatted) raw_html = f.read().replace("{{ date }}", date_formatted)
soup = BeautifulSoup(raw_html, 'html.parser') soup = BeautifulSoup(raw_html, "html.parser")
posts = [] posts = []
@@ -107,7 +110,7 @@ def render_curl(date=None):
for line in text.splitlines(): for line in text.splitlines():
while len(line) > MAX_WIDTH: while len(line) > MAX_WIDTH:
# Find last space within max_width # Find last space within max_width
split_at = line.rfind(' ', 0, MAX_WIDTH) split_at = line.rfind(" ", 0, MAX_WIDTH)
if split_at == -1: if split_at == -1:
split_at = MAX_WIDTH split_at = MAX_WIDTH
wrapped_lines.append(line[:split_at].rstrip()) wrapped_lines.append(line[:split_at].rstrip())
@@ -128,8 +131,9 @@ def render_curl(date=None):
for post in posts: for post in posts:
response += f"{post['header']}\n\n{post['content']}\n\n" response += f"{post['header']}\n\n{post['content']}\n\n"
return render_template("now.ascii", date=date_formatted, content=response, header=get_header()) return render_template(
"now.ascii", date=date_formatted, content=response, header=get_header()
)
@app.route("/", strict_slashes=False) @app.route("/", strict_slashes=False)
@@ -157,8 +161,9 @@ def old():
date_fmt = datetime.datetime.strptime(date, "%y_%m_%d") date_fmt = datetime.datetime.strptime(date, "%y_%m_%d")
date_fmt = date_fmt.strftime("%A, %B %d, %Y") date_fmt = date_fmt.strftime("%A, %B %d, %Y")
response += f"{date_fmt} - /now/{link}\n" response += f"{date_fmt} - /now/{link}\n"
return render_template("now.ascii", date="Old Now Pages", content=response, header=get_header()) return render_template(
"now.ascii", date="Old Now Pages", content=response, header=get_header()
)
html = '<ul class="list-group">' html = '<ul class="list-group">'
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_date(True)}</li></a>' html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_date(True)}</li></a>'
@@ -171,7 +176,9 @@ def old():
html += "</ul>" html += "</ul>"
return render_template( return render_template(
"now/old.html", handshake_scripts=getHandshakeScript(request.host), now_pages=html "now/old.html",
handshake_scripts=getHandshakeScript(request.host),
now_pages=html,
) )
@@ -189,7 +196,7 @@ def rss():
link = page.strip(".html") link = page.strip(".html")
date = datetime.datetime.strptime(link, "%y_%m_%d") date = datetime.datetime.strptime(link, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y") date = date.strftime("%A, %B %d, %Y")
rss += f'<item><title>What\'s Happening {date}</title><link>{host}/now/{link}</link><description>Latest updates for {date}</description><guid>{host}/now/{link}</guid></item>' rss += f"<item><title>What's Happening {date}</title><link>{host}/now/{link}</link><description>Latest updates for {date}</description><guid>{host}/now/{link}</guid></item>"
rss += "</channel></rss>" rss += "</channel></rss>"
return make_response(rss, 200, {"Content-Type": "application/rss+xml"}) return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
@@ -200,6 +207,17 @@ def json():
host = "https://" + request.host host = "https://" + request.host
if ":" in request.host: if ":" in request.host:
host = "http://" + request.host host = "http://" + request.host
now_pages = [{"url": host+"/now/"+page.strip(".html"), "date": datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime( now_pages = [
"%A, %B %d, %Y"), "title": "What's Happening "+datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime("%A, %B %d, %Y")} for page in now_pages] {
"url": host + "/now/" + page.strip(".html"),
"date": datetime.datetime.strptime(
page.strip(".html"), "%y_%m_%d"
).strftime("%A, %B %d, %Y"),
"title": "What's Happening "
+ datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime(
"%A, %B %d, %Y"
),
}
for page in now_pages
]
return jsonify(now_pages) return jsonify(now_pages)

View File

@@ -2,7 +2,8 @@ from flask import Blueprint, make_response, request
from tools import error_response from tools import error_response
import requests import requests
app = Blueprint('podcast', __name__) app = Blueprint("podcast", __name__)
@app.route("/ID1") @app.route("/ID1")
def index(): def index():

View File

@@ -9,12 +9,12 @@ import binascii
import base64 import base64
import os import os
app = Blueprint('sol', __name__) app = Blueprint("sol", __name__)
SOLANA_HEADERS = { SOLANA_HEADERS = {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Action-Version": "2.4.2", "X-Action-Version": "2.4.2",
"X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" "X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
} }
SOLANA_ADDRESS = None SOLANA_ADDRESS = None
@@ -23,15 +23,19 @@ if os.path.isfile(".well-known/wallets/SOL"):
address = file.read() address = file.read()
SOLANA_ADDRESS = Pubkey.from_string(address.strip()) SOLANA_ADDRESS = Pubkey.from_string(address.strip())
def create_transaction(sender_address: str, amount: float) -> str: def create_transaction(sender_address: str, amount: float) -> str:
if SOLANA_ADDRESS is None: 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.") raise ValueError(
"SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address."
)
# Create transaction # Create transaction
sender = Pubkey.from_string(sender_address) sender = Pubkey.from_string(sender_address)
transfer_ix = transfer( transfer_ix = transfer(
TransferParams( TransferParams(
from_pubkey=sender, to_pubkey=SOLANA_ADDRESS, lamports=int( from_pubkey=sender,
amount * 1000000000) to_pubkey=SOLANA_ADDRESS,
lamports=int(amount * 1000000000),
) )
) )
solana_client = Client("https://api.mainnet-beta.solana.com") solana_client = Client("https://api.mainnet-beta.solana.com")
@@ -50,11 +54,15 @@ def create_transaction(sender_address: str, amount: float) -> str:
base64_string = base64.b64encode(raw_bytes).decode("utf-8") base64_string = base64.b64encode(raw_bytes).decode("utf-8")
return base64_string return base64_string
def get_solana_address() -> str: def get_solana_address() -> str:
if SOLANA_ADDRESS is None: 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.") raise ValueError(
"SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address."
)
return str(SOLANA_ADDRESS) return str(SOLANA_ADDRESS)
@app.route("/donate", methods=["GET", "OPTIONS"]) @app.route("/donate", methods=["GET", "OPTIONS"])
def sol_donate(): def sol_donate():
data = { data = {
@@ -103,7 +111,6 @@ def sol_donate_amount(amount):
@app.route("/donate/<amount>", methods=["POST"]) @app.route("/donate/<amount>", methods=["POST"])
def sol_donate_post(amount): def sol_donate_post(amount):
if not request.json: if not request.json:
return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS
@@ -122,4 +129,8 @@ def sol_donate_post(amount):
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
transaction = create_transaction(sender, amount) transaction = create_transaction(sender, amount)
return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS return (
jsonify({"message": "Success", "transaction": transaction}),
200,
SOLANA_HEADERS,
)

View File

@@ -5,7 +5,7 @@ import requests
import time import time
import base64 import base64
app = Blueprint('spotify', __name__, url_prefix='/spotify') app = Blueprint("spotify", __name__, url_prefix="/spotify")
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID") CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET") CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
@@ -21,6 +21,7 @@ ACCESS_TOKEN = None
REFRESH_TOKEN = os.getenv("SPOTIFY_REFRESH_TOKEN") REFRESH_TOKEN = os.getenv("SPOTIFY_REFRESH_TOKEN")
TOKEN_EXPIRES = 0 TOKEN_EXPIRES = 0
def refresh_access_token(): def refresh_access_token():
"""Refresh Spotify access token when expired.""" """Refresh Spotify access token when expired."""
global ACCESS_TOKEN, TOKEN_EXPIRES global ACCESS_TOKEN, TOKEN_EXPIRES
@@ -52,6 +53,7 @@ def refresh_access_token():
TOKEN_EXPIRES = time.time() + token_info.get("expires_in", 3600) TOKEN_EXPIRES = time.time() + token_info.get("expires_in", 3600)
return ACCESS_TOKEN return ACCESS_TOKEN
@app.route("/login") @app.route("/login")
def login(): def login():
auth_query = ( auth_query = (
@@ -60,6 +62,7 @@ def login():
) )
return redirect(auth_query) return redirect(auth_query)
@app.route("/callback") @app.route("/callback")
def callback(): def callback():
code = request.args.get("code") code = request.args.get("code")
@@ -76,12 +79,14 @@ def callback():
response = requests.post(SPOTIFY_TOKEN_URL, data=data) response = requests.post(SPOTIFY_TOKEN_URL, data=data)
token_info = response.json() token_info = response.json()
if "access_token" not in token_info: if "access_token" not in token_info:
return json_response(request, {"error": "Failed to obtain token", "details": token_info}, 400) return json_response(
request, {"error": "Failed to obtain token", "details": token_info}, 400
)
access_token = token_info["access_token"] access_token = token_info["access_token"]
me = requests.get( me = requests.get(
"https://api.spotify.com/v1/me", "https://api.spotify.com/v1/me",
headers={"Authorization": f"Bearer {access_token}"} headers={"Authorization": f"Bearer {access_token}"},
).json() ).json()
if me.get("id") != ALLOWED_SPOTIFY_USER_ID: if me.get("id") != ALLOWED_SPOTIFY_USER_ID:
@@ -93,12 +98,14 @@ def callback():
print("Refresh Token:", REFRESH_TOKEN) print("Refresh Token:", REFRESH_TOKEN)
return redirect(url_for("spotify.currently_playing")) return redirect(url_for("spotify.currently_playing"))
@app.route("/", strict_slashes=False) @app.route("/", strict_slashes=False)
@app.route("/playing") @app.route("/playing")
def currently_playing(): def currently_playing():
"""Public endpoint showing your current track.""" """Public endpoint showing your current track."""
track = get_spotify_track() track = get_spotify_track()
return json_response(request, {"spotify":track}, 200) return json_response(request, {"spotify": track}, 200)
def get_spotify_track(): def get_spotify_track():
"""Internal function to get current playing track without HTTP context.""" """Internal function to get current playing track without HTTP context."""
@@ -124,7 +131,7 @@ def get_spotify_track():
"album_name": data["item"]["album"]["name"], "album_name": data["item"]["album"]["name"],
"album_art": data["item"]["album"]["images"][0]["url"], "album_art": data["item"]["album"]["images"][0]["url"],
"is_playing": data["is_playing"], "is_playing": data["is_playing"],
"progress_ms": data.get("progress_ms",0), "progress_ms": data.get("progress_ms", 0),
"duration_ms": data["item"].get("duration_ms",1) "duration_ms": data["item"].get("duration_ms", 1),
} }
return track return track

View File

@@ -1,7 +1,7 @@
from flask import Blueprint, request from flask import Blueprint, request
from tools import json_response from tools import json_response
app = Blueprint('template', __name__) app = Blueprint("template", __name__)
@app.route("/", strict_slashes=False) @app.route("/", strict_slashes=False)

View File

@@ -1,8 +1,15 @@
from flask import Blueprint, 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 from tools import error_response
import os import os
app = Blueprint('well-known', __name__, url_prefix='/.well-known') app = Blueprint("well-known", __name__, url_prefix="/.well-known")
@app.route("/<path:path>") @app.route("/<path:path>")
@@ -12,7 +19,7 @@ def index(path):
@app.route("/wallets/<path:path>") @app.route("/wallets/<path:path>")
def wallets(path): def wallets(path):
if path[0] == "." and 'proof' not in path: if path[0] == "." and "proof" not in path:
return send_from_directory( return send_from_directory(
".well-known/wallets", path, mimetype="application/json" ".well-known/wallets", path, mimetype="application/json"
) )

View File

@@ -2,6 +2,7 @@
Cache helper module for expensive API calls and configuration. Cache helper module for expensive API calls and configuration.
Provides centralized caching with TTL for external API calls. Provides centralized caching with TTL for external API calls.
""" """
import datetime import datetime
import os import os
import json import json
@@ -26,14 +27,17 @@ def get_nc_config():
current_time = datetime.datetime.now().timestamp() current_time = datetime.datetime.now().timestamp()
# Check if cache is valid # Check if cache is valid
if _nc_config_cache["data"] and (current_time - _nc_config_cache["timestamp"]) < _nc_config_ttl: if (
_nc_config_cache["data"]
and (current_time - _nc_config_cache["timestamp"]) < _nc_config_ttl
):
return _nc_config_cache["data"] return _nc_config_cache["data"]
# Fetch new config # Fetch new config
try: try:
config = requests.get( config = requests.get(
"https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json", "https://cloud.woodburn.au/s/4ToXgFe3TnnFcN7/download/website-conf.json",
timeout=5 timeout=5,
).json() ).json()
_nc_config_cache = {"data": config, "timestamp": current_time} _nc_config_cache = {"data": config, "timestamp": current_time}
return config return config
@@ -61,15 +65,20 @@ def get_git_latest_activity():
current_time = datetime.datetime.now().timestamp() current_time = datetime.datetime.now().timestamp()
# Check if cache is valid # Check if cache is valid
if _git_data_cache["data"] and (current_time - _git_data_cache["timestamp"]) < _git_data_ttl: if (
_git_data_cache["data"]
and (current_time - _git_data_cache["timestamp"]) < _git_data_ttl
):
return _git_data_cache["data"] return _git_data_cache["data"]
# Fetch new data # Fetch new data
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_AUTH") or os.getenv("git_token") or ""}, headers={
timeout=5 "Authorization": os.getenv("GIT_AUTH") or os.getenv("git_token") or ""
},
timeout=5,
) )
git_data = git.json() git_data = git.json()
if git_data and len(git_data) > 0: if git_data and len(git_data) > 0:
@@ -111,15 +120,17 @@ def get_projects(limit=3):
current_time = datetime.datetime.now().timestamp() current_time = datetime.datetime.now().timestamp()
# Check if cache is valid # Check if cache is valid
if _projects_cache["data"] and (current_time - _projects_cache["timestamp"]) < _projects_ttl: if (
_projects_cache["data"]
and (current_time - _projects_cache["timestamp"]) < _projects_ttl
):
return _projects_cache["data"][:limit] return _projects_cache["data"][:limit]
# Fetch new data # Fetch new data
try: try:
projects = [] projects = []
projectsreq = requests.get( projectsreq = requests.get(
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos", "https://git.woodburn.au/api/v1/users/nathanwoodburn/repos", timeout=5
timeout=5
) )
projects = projectsreq.json() projects = projectsreq.json()
@@ -128,7 +139,7 @@ def get_projects(limit=3):
while 'rel="next"' in projectsreq.headers.get("link", ""): while 'rel="next"' in projectsreq.headers.get("link", ""):
projectsreq = requests.get( projectsreq = requests.get(
f"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page={pageNum}", f"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page={pageNum}",
timeout=5 timeout=5,
) )
projects += projectsreq.json() projects += projectsreq.json()
pageNum += 1 pageNum += 1
@@ -143,7 +154,9 @@ def get_projects(limit=3):
project["name"] = project["name"].replace("_", " ").replace("-", " ") project["name"] = project["name"].replace("_", " ").replace("-", " ")
# Sort by last updated # Sort by last updated
projects_sorted = sorted(projects, key=lambda x: x.get("updated_at", ""), reverse=True) projects_sorted = sorted(
projects, key=lambda x: x.get("updated_at", ""), reverse=True
)
# Remove duplicates by name # Remove duplicates by name
seen_names = set() seen_names = set()
@@ -178,14 +191,16 @@ def get_uptime_status():
current_time = datetime.datetime.now().timestamp() current_time = datetime.datetime.now().timestamp()
# Check if cache is valid # Check if cache is valid
if _uptime_cache["data"] is not None and (current_time - _uptime_cache["timestamp"]) < _uptime_ttl: if (
_uptime_cache["data"] is not None
and (current_time - _uptime_cache["timestamp"]) < _uptime_ttl
):
return _uptime_cache["data"] return _uptime_cache["data"]
# Fetch new data # Fetch new data
try: try:
uptime = requests.get( uptime = requests.get(
"https://uptime.woodburn.au/api/status-page/main/badge", "https://uptime.woodburn.au/api/status-page/main/badge", timeout=5
timeout=5
) )
content = uptime.content.decode("utf-8").lower() content = uptime.content.decode("utf-8").lower()
status = "maintenance" in content or uptime.content.count(b"Up") > 1 status = "maintenance" in content or uptime.content.count(b"Up") > 1
@@ -247,4 +262,3 @@ def get_wallet_domains():
except Exception as e: except Exception as e:
print(f"Error loading domains: {e}") print(f"Error loading domains: {e}")
return {} return {}

View File

@@ -1,36 +1,37 @@
import os import os
def cleanSite(path:str):
def cleanSite(path: str):
# Check if the file is sitemap.xml # Check if the file is sitemap.xml
if path.endswith('sitemap.xml'): if path.endswith("sitemap.xml"):
# Open the file # Open the file
with open(path, 'r') as f: with open(path, "r") as f:
# Read the content # Read the content
content = f.read() content = f.read()
# Replace all .html with empty string # Replace all .html with empty string
content = content.replace('.html', '') content = content.replace(".html", "")
# Write the content back to the file # Write the content back to the file
with open(path, 'w') as f: with open(path, "w") as f:
f.write(content) f.write(content)
# Skip the file # Skip the file
return return
# If the file is not an html file, skip it # If the file is not an html file, skip it
if not path.endswith('.html'): if not path.endswith(".html"):
if os.path.isdir(path): if os.path.isdir(path):
for file in os.listdir(path): for file in os.listdir(path):
cleanSite(path + '/' + file) cleanSite(path + "/" + file)
return return
# Open the file # Open the file
with open(path, 'r') as f: with open(path, "r") as f:
# Read and remove all .html # Read and remove all .html
content = f.read().replace('.html"', '"') content = f.read().replace('.html"', '"')
# Write the cleaned content back to the file # Write the cleaned content back to the file
with open(path, 'w') as f: with open(path, "w") as f:
f.write(content) f.write(content)
for file in os.listdir('templates'): for file in os.listdir("templates"):
cleanSite('templates/' + file) cleanSite("templates/" + file)

89
curl.py
View File

@@ -8,7 +8,8 @@ from cache_helper import get_git_latest_activity, get_projects as get_projects_c
MAX_WIDTH = 80 MAX_WIDTH = 80
def clean_path(path:str):
def clean_path(path: str):
path = path.strip("/ ").lower() path = path.strip("/ ").lower()
# Strip any .html extension # Strip any .html extension
if path.endswith(".html"): if path.endswith(".html"):
@@ -19,19 +20,21 @@ def clean_path(path:str):
path = "index" path = "index"
return path return path
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def get_header(): 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=16) @lru_cache(maxsize=16)
def get_current_project(): def get_current_project():
git = get_git_latest_activity() git = get_git_latest_activity()
repo_name = git["repo"]["name"].lower() repo_name = git["repo"]["name"].lower()
repo_description = git["repo"]["description"] repo_description = git["repo"]["description"]
if not repo_description: if not repo_description:
return f"[1;36m{repo_name}[0m" return f"{repo_name}"
return f"[1;36m{repo_name}[0m - [1m{repo_description}[0m" return f"{repo_name} - {repo_description}"
@lru_cache(maxsize=16) @lru_cache(maxsize=16)
@@ -39,11 +42,13 @@ def get_projects():
projects_data = get_projects_cached(limit=5) projects_data = get_projects_cached(limit=5)
projects = "" projects = ""
for project in projects_data: for project in projects_data:
projects += f"""[1m{project['name']}[0m - {project['description'] if project['description'] else 'No description'} projects += f"""{project["name"]} - {project["description"] if project["description"] else "No description"}
{project['html_url']} {project["html_url"]}
""" """
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)
@@ -51,39 +56,81 @@ def curl_response(request):
# Handle special cases # Handle special cases
if path == "index": if path == "index":
# Get current project # 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'} 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": if path == "projects":
# Get projects # Get projects
return render_template("projects.ascii",header=get_header(),projects=get_projects()), 200, {'Content-Type': 'text/plain; charset=utf-8'} return (
render_template(
"projects.ascii", header=get_header(), projects=get_projects()
),
200,
{"Content-Type": "text/plain; charset=utf-8"},
)
if path == "donate": if path == "donate":
# Get donation info # Get donation info
return render_template("donate.ascii",header=get_header(), return (
HNS=getAddress("HNS"), BTC=getAddress("BTC"), render_template(
SOL=getAddress("SOL"), ETH=getAddress("ETH") "donate.ascii",
), 200, {'Content-Type': 'text/plain; charset=utf-8'} 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": if path == "donate/more":
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] != "."]
coinList.sort() coinList.sort()
return render_template("donate_more.ascii",header=get_header(), return (
coins=coinList render_template("donate_more.ascii", header=get_header(), coins=coinList),
), 200, {'Content-Type': 'text/plain; charset=utf-8'} 200,
{"Content-Type": "text/plain; charset=utf-8"},
)
# For other donation pages, fall back to ascii if it exists # For other donation pages, fall back to ascii if it exists
if path.startswith("donate/"): if path.startswith("donate/"):
coin = path.split("/")[1] coin = path.split("/")[1]
address = getAddress(coin) address = getAddress(coin)
if address != "": if address != "":
return render_template("donate_coin.ascii",header=get_header(),coin=coin.upper(),address=address), 200, {'Content-Type': 'text/plain; charset=utf-8'} 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": if path == "tools":
tools = get_tools_data() tools = get_tools_data()
return render_template("tools.ascii",header=get_header(),tools=tools), 200, {'Content-Type': 'text/plain; charset=utf-8'} return (
render_template("tools.ascii", header=get_header(), tools=tools),
200,
{"Content-Type": "text/plain; charset=utf-8"},
)
if os.path.exists(f"templates/{path}.ascii"): if os.path.exists(f"templates/{path}.ascii"):
return render_template(f"{path}.ascii",header=get_header()), 200, {'Content-Type': 'text/plain; charset=utf-8'} return (
render_template(f"{path}.ascii", header=get_header()),
200,
{"Content-Type": "text/plain; charset=utf-8"},
)
# Fallback to html if it exists # Fallback to html if it exists
if os.path.exists(f"templates/{path}.html"): if os.path.exists(f"templates/{path}.html"):
@@ -92,6 +139,10 @@ def curl_response(request):
# Return curl error page # Return curl error page
error = { error = {
"code": 404, "code": 404,
"message": "The requested resource was not found on this server." "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'} return (
render_template("error.ascii", header=get_header(), error=error),
404,
{"Content-Type": "text/plain; charset=utf-8"},
)

57
mail.py
View File

@@ -21,6 +21,7 @@ import os
# "body":"G'\''day\nThis is a test email from my website api\n\nRegards,\nNathan.Woodburn/" # "body":"G'\''day\nThis is a test email from my website api\n\nRegards,\nNathan.Woodburn/"
# }' # }'
def validateSender(email): def validateSender(email):
domains = os.getenv("EMAIL_DOMAINS") domains = os.getenv("EMAIL_DOMAINS")
if not domains: if not domains:
@@ -33,37 +34,29 @@ def validateSender(email):
return False return False
def sendEmail(data): def sendEmail(data):
fromEmail = "noreply@woodburn.au" fromEmail = "noreply@woodburn.au"
if "from" in data: if "from" in data:
fromEmail = data["from"] fromEmail = data["from"]
if not validateSender(fromEmail): if not validateSender(fromEmail):
return jsonify({ return jsonify({"status": 400, "message": "Bad request 'from' email invalid"})
"status": 400,
"message": "Bad request 'from' email invalid"
})
if "to" not in data: if "to" not in data:
return jsonify({ return jsonify({"status": 400, "message": "Bad request 'to' json data missing"})
"status": 400,
"message": "Bad request 'to' json data missing"
})
to = data["to"] to = data["to"]
if "subject" not in data: if "subject" not in data:
return jsonify({ return jsonify(
"status": 400, {"status": 400, "message": "Bad request 'subject' json data missing"}
"message": "Bad request 'subject' json data missing" )
})
subject = data["subject"] subject = data["subject"]
if "body" not in data: if "body" not in data:
return jsonify({ return jsonify(
"status": 400, {"status": 400, "message": "Bad request 'body' json data missing"}
"message": "Bad request 'body' json data missing" )
})
body = data["body"] body = data["body"]
if not re.match(r"[^@]+@[^@]+\.[^@]+", to): if not re.match(r"[^@]+@[^@]+\.[^@]+", to):
@@ -76,15 +69,15 @@ def sendEmail(data):
raise ValueError("Body cannot be empty.") raise ValueError("Body cannot be empty.")
fromName = "Nathan Woodburn" fromName = "Nathan Woodburn"
if 'sender' in data: if "sender" in data:
fromName = data['sender'] fromName = data["sender"]
# Create the email message # Create the email message
msg = MIMEMultipart() msg = MIMEMultipart()
msg['From'] = formataddr((fromName, fromEmail)) msg["From"] = formataddr((fromName, fromEmail))
msg['To'] = to msg["To"] = to
msg['Subject'] = subject msg["Subject"] = subject
msg.attach(MIMEText(body, 'plain')) msg.attach(MIMEText(body, "plain"))
# Sending the email # Sending the email
try: try:
@@ -92,24 +85,12 @@ def sendEmail(data):
user = os.getenv("EMAIL_USER") user = os.getenv("EMAIL_USER")
password = os.getenv("EMAIL_PASS") password = os.getenv("EMAIL_PASS")
if host is None or user is None or password is None: if host is None or user is None or password is None:
return jsonify({ return jsonify({"status": 500, "error": "Email server not configured"})
"status": 500,
"error": "Email server not configured"
})
with smtplib.SMTP_SSL(host, 465) as server: with smtplib.SMTP_SSL(host, 465) as server:
server.login(user, password) server.login(user, password)
server.sendmail(fromEmail, to, msg.as_string()) server.sendmail(fromEmail, to, msg.as_string())
print("Email sent successfully.") print("Email sent successfully.")
return jsonify({ return jsonify({"status": 200, "message": "Send email successfully"})
"status": 200,
"message": "Send email successfully"
})
except Exception as e: except Exception as e:
return jsonify({ return jsonify({"status": 500, "error": "Sending email failed", "exception": e})
"status": 500,
"error": "Sending email failed",
"exception":e
})

22
main.py
View File

@@ -17,9 +17,10 @@ class GunicornApp(BaseApplication):
def load(self): def load(self):
return self.application return self.application
if __name__ == '__main__':
workers = os.getenv('WORKERS') if __name__ == "__main__":
threads = os.getenv('THREADS') workers = os.getenv("WORKERS")
threads = os.getenv("THREADS")
if workers is None: if workers is None:
workers = 1 workers = 1
if threads is None: if threads is None:
@@ -27,10 +28,17 @@ if __name__ == '__main__':
workers = int(workers) workers = int(workers)
threads = int(threads) threads = int(threads)
options = { options = {
'bind': '0.0.0.0:5000', "bind": "0.0.0.0:5000",
'workers': workers, "workers": workers,
'threads': threads, "threads": threads,
} }
gunicorn_app = GunicornApp(app, options) gunicorn_app = GunicornApp(app, options)
print('Starting server with ' + str(workers) + ' workers and ' + str(threads) + ' threads', flush=True) print(
"Starting server with "
+ str(workers)
+ " workers and "
+ str(threads)
+ " threads",
flush=True,
)
gunicorn_app.run() gunicorn_app.run()

View File

@@ -363,7 +363,11 @@ def donate():
for token in tokenList: for token in tokenList:
chain_display = f" on {token['chain']}" if token["chain"] != "null" else "" chain_display = f" on {token['chain']}" if token["chain"] != "null" else ""
symbol_display = f" ({token['symbol']}{chain_display})" if token["symbol"] != token["name"] else chain_display symbol_display = (
f" ({token['symbol']}{chain_display})"
if token["symbol"] != token["name"]
else chain_display
)
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"]}{symbol_display}</a>'
crypto = request.args.get("c") crypto = request.args.get("c")
@@ -404,8 +408,12 @@ def donate():
if not token: if not token:
cryptoHTML += f"<br>Donate with {coin_display}:" cryptoHTML += f"<br>Donate with {coin_display}:"
else: else:
token_symbol = f" ({token['symbol']})" if token['symbol'] != token['name'] else "" token_symbol = (
cryptoHTML += f"<br>Donate with {token['name']}{token_symbol} on {crypto}:" 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:
@@ -413,9 +421,13 @@ def donate():
elif token: elif token:
if "address" in token: if "address" in token:
address = token["address"] address = token["address"]
token_symbol = f" ({token['symbol']})" if token['symbol'] != token['name'] else "" token_symbol = (
chain_display = f" on {crypto}" if crypto != 'NULL' else "" f" ({token['symbol']})" if token["symbol"] != token["name"] else ""
cryptoHTML += f"<br>Donate with {token['name']}{token_symbol}{chain_display}:" )
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

View File

@@ -24,17 +24,10 @@ CRAWLERS = [
"Exabot", "Exabot",
"facebot", "facebot",
"ia_archiver", "ia_archiver",
"Twitterbot" "Twitterbot",
] ]
CLI_AGENTS = [ CLI_AGENTS = ["curl", "hurl", "xh", "Posting", "HTTPie", "nushell"]
"curl",
"hurl",
"xh",
"Posting",
"HTTPie",
"nushell"
]
def getClientIP(request: Request) -> str: def getClientIP(request: Request) -> str:
@@ -56,6 +49,7 @@ def getClientIP(request: Request) -> str:
ip = "unknown" ip = "unknown"
return ip return ip
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def getGitCommit() -> str: def getGitCommit() -> str:
""" """
@@ -115,6 +109,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
@lru_cache(maxsize=128) @lru_cache(maxsize=128)
def isDev(host: str) -> bool: def isDev(host: str) -> bool:
""" """
@@ -135,6 +130,7 @@ def isDev(host: str) -> bool:
return True return True
return False return False
@lru_cache(maxsize=128) @lru_cache(maxsize=128)
def getHandshakeScript(host: str) -> str: def getHandshakeScript(host: str) -> str:
""" """
@@ -150,6 +146,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>'
@lru_cache(maxsize=64) @lru_cache(maxsize=64)
def getAddress(coin: str) -> str: def getAddress(coin: str) -> str:
""" """
@@ -187,7 +184,9 @@ def getFilePath(name: str, path: str) -> Optional[str]:
return None return None
def json_response(request: Request, message: Union[str, Dict] = "404 Not Found", code: int = 404): def json_response(
request: Request, message: Union[str, Dict] = "404 Not Found", code: int = 404
):
""" """
Create a JSON response with standard formatting. Create a JSON response with standard formatting.
@@ -205,17 +204,20 @@ def json_response(request: Request, message: Union[str, Dict] = "404 Not Found",
message["ip"] = getClientIP(request) message["ip"] = getClientIP(request)
return jsonify(message), code return jsonify(message), code
return jsonify({ return jsonify(
{
"status": code, "status": code,
"message": message, "message": message,
"ip": getClientIP(request), "ip": getClientIP(request),
}), code }
), code
def error_response( def error_response(
request: Request, request: Request,
message: str = "404 Not Found", message: str = "404 Not Found",
code: int = 404, code: int = 404,
force_json: bool = False force_json: bool = False,
) -> Union[Tuple[Dict, int], object]: ) -> Union[Tuple[Dict, int], object]:
""" """
Create an error response in JSON or HTML format. Create an error response in JSON or HTML format.
@@ -233,10 +235,12 @@ def error_response(
return json_response(request, message, code) return json_response(request, message, code)
# Check if <error code>.html exists in templates # Check if <error code>.html exists in templates
template_name = f"{code}.html" if os.path.isfile( template_name = (
f"templates/{code}.html") else "404.html" f"{code}.html" if os.path.isfile(f"templates/{code}.html") else "404.html"
response = make_response(render_template( )
template_name, code=code, message=message), code) response = make_response(
render_template(template_name, code=code, message=message), code
)
# Add message to response headers # Add message to response headers
response.headers["X-Error-Message"] = message response.headers["X-Error-Message"] = message
@@ -260,8 +264,7 @@ def parse_date(date_groups: list[str]) -> str | None:
date_str = " ".join(date_groups).strip() date_str = " ".join(date_groups).strip()
# Remove ordinal suffixes # Remove ordinal suffixes
date_str = re.sub(r'(\d+)(st|nd|rd|th)', r'\1', date_str = re.sub(r"(\d+)(st|nd|rd|th)", r"\1", date_str, flags=re.IGNORECASE)
date_str, flags=re.IGNORECASE)
# Parse with dateutil, default day=1 if missing # Parse with dateutil, default day=1 if missing
dt = parse(date_str, default=datetime.datetime(1900, 1, 1)) dt = parse(date_str, default=datetime.datetime(1900, 1, 1))
@@ -275,6 +278,7 @@ def parse_date(date_groups: list[str]) -> str | None:
except (ValueError, TypeError): except (ValueError, TypeError):
return None return None
def get_tools_data(): def get_tools_data():
with open("data/tools.json", "r") as f: with open("data/tools.json", "r") as f:
return json.load(f) return json.load(f)