227 Commits

Author SHA1 Message Date
53e05922bf fix: Add curl to dockerfiel for CD healthchecks
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m17s
Build Docker / BuildImage (push) Successful in 2m6s
2025-11-03 13:51:01 +11:00
0be0dad1b2 feat: Add new dockerfile to shrink image
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m57s
Build Docker / BuildImage (push) Successful in 3m42s
2025-11-03 13:44:19 +11:00
f404d55935 feat: Add phone number and city to resume
All checks were successful
Build Docker / BuildImage (push) Successful in 1m5s
Check Code Quality / RuffCheck (push) Successful in 1m8s
2025-11-01 18:03:03 +11:00
009c2b430c feat: Update projects
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m22s
Build Docker / BuildImage (push) Successful in 1m39s
2025-11-01 13:19:15 +11:00
ed96fbcc29 fix: Add curl to Dockerfile for coolify healthchecks
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m19s
Build Docker / BuildImage (push) Successful in 2m11s
2025-11-01 12:49:45 +11:00
4555ef5da2 Merge pull request 'Add new uv python3 manager' (#5) from feat/uv into main
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
Check Code Quality / RuffCheck (push) Successful in 1m7s
Reviewed-on: #5
2025-11-01 12:41:28 +11:00
1335a73eb6 fix: Remove requirement.txt to stop nixpacks using pip
All checks were successful
Build Docker / BuildImage (push) Successful in 1m6s
Check Code Quality / RuffCheck (push) Successful in 1m17s
2025-11-01 12:31:47 +11:00
b8f3039629 feat: Update Dockerfile to use new UV system
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m44s
Build Docker / BuildImage (push) Successful in 3m15s
Also fixes error in spotify refreshing
2025-11-01 12:17:25 +11:00
1888160fa5 fix: Update pyproject version to match latest git tag 2025-11-01 11:50:51 +11:00
7dd0f839cf feat: Add initial uv package info 2025-11-01 11:44:44 +11:00
5a0068586a feat: Cleanup resume summary
All checks were successful
Build Docker / BuildImage (push) Successful in 1m3s
Check Code Quality / RuffCheck (push) Successful in 1m8s
2025-10-31 15:16:48 +11:00
8079780c08 Merge pull request 'Refactor blueprints to make them easier to import' (#4) from feat/blueprint_imports into main
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m5s
Build Docker / BuildImage (push) Successful in 1m10s
Reviewed-on: #4
2025-10-30 21:44:46 +11:00
72b8dae35e fix: Update curl tools page to use new demo data
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
Check Code Quality / RuffCheck (push) Successful in 1m11s
2025-10-30 21:35:48 +11:00
323ace5775 feat: Remove old demo scripts
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 56s
Build Docker / BuildImage (push) Successful in 1m2s
2025-10-30 21:34:35 +11:00
c2803e372a feat: Dynamically load tool demos and start tracking BSdesign
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 58s
Build Docker / BuildImage (push) Successful in 1m0s
2025-10-30 21:29:27 +11:00
2a9e704f29 feat: Add zellij demo
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 59s
Build Docker / BuildImage (push) Successful in 1m4s
2025-10-30 20:52:20 +11:00
0c490625a9 fix: Add apt update before install python
All checks were successful
Build Docker / BuildImage (push) Successful in 47s
Check Code Quality / RuffCheck (push) Successful in 58s
2025-10-30 20:39:27 +11:00
b9753617ad fix: Remove sudo from action
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 17s
Build Docker / BuildImage (push) Successful in 1m2s
2025-10-30 20:38:09 +11:00
b87d19c5d9 fix: Manually install python3
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 13s
Build Docker / BuildImage (push) Successful in 47s
2025-10-30 20:36:29 +11:00
67e8b4cf7e fix: Try using a new container
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 31s
Build Docker / BuildImage (push) Successful in 58s
2025-10-30 20:33:02 +11:00
bfc6652f29 fix: Install python first
Some checks failed
Build Docker / BuildImage (push) Successful in 55s
Check Code Quality / RuffCheck (push) Failing after 1m35s
2025-10-30 20:28:31 +11:00
38372c0cff fix: Manually install ruff
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 17s
Build Docker / BuildImage (push) Successful in 58s
2025-10-30 20:27:00 +11:00
dd64313006 fix: Set action git url
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 22s
Build Docker / BuildImage (push) Successful in 47s
2025-10-30 20:19:56 +11:00
9e20a6171a feat: Add ruff check
Some checks failed
Check Code Quality / RuffCheck (push) Failing after 25s
Build Docker / BuildImage (push) Successful in 57s
2025-10-30 20:15:10 +11:00
da347fd860 feat: Add max width for Now page contents
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
2025-10-30 20:06:32 +11:00
776b7de753 fix: Replace actions.json in the main server.py
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
2025-10-30 19:58:29 +11:00
7b2b3659bb fix: Remove strict slashes from index routes
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
2025-10-30 19:50:03 +11:00
872373dffd feat: Remove strict slashes for now pages 2025-10-30 19:44:01 +11:00
8d832372cd feat: Add curl support for now pages
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
2025-10-30 19:36:43 +11:00
03dae87272 feat: Refactor blueprints to make them easier to import
All checks were successful
Build Docker / BuildImage (push) Successful in 47s
2025-10-30 18:22:21 +11:00
4c654fcb78 feat: Add HTTPie to CLI agents
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
2025-10-30 17:26:06 +11:00
c9542e4af7 fix: Remove X- headers
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
2025-10-30 17:18:50 +11:00
e184375897 feat: Add Posting to CLI tools
All checks were successful
Build Docker / BuildImage (push) Successful in 59s
2025-10-30 17:12:43 +11:00
844f1b52e2 feat: Update isCurl to isCLI to allow more CLI agents 2025-10-30 17:07:20 +11:00
19c51c3665 feat: Add header route for troubleshooting 2025-10-30 17:03:17 +11:00
85ebd460ed feat: Add progress bar to spotify widget
All checks were successful
Build Docker / BuildImage (push) Successful in 1m57s
2025-10-30 11:45:04 +11:00
50879b4f0e feat: Add Vesktop to desktop applications
All checks were successful
Build Docker / BuildImage (push) Successful in 2m0s
2025-10-29 14:59:08 +11:00
6c09923281 feat: Add curl error page and new tests
All checks were successful
Build Docker / BuildImage (push) Successful in 52s
2025-10-28 15:04:21 +11:00
332c408b89 fix: Animation for spotify toggle
All checks were successful
Build Docker / BuildImage (push) Successful in 2m8s
2025-10-28 14:46:09 +11:00
4ae6f7bb99 feat: Optimise profile image
All checks were successful
Build Docker / BuildImage (push) Successful in 55s
2025-10-26 22:24:28 +11:00
d4d6b47225 feat: Add spotify to api routes
All checks were successful
Build Docker / BuildImage (push) Successful in 55s
2025-10-26 21:31:30 +11:00
9809fe0695 feat: Add spotify to curl route
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
2025-10-26 21:14:55 +11:00
3522389422 fix: Improve Spotify widget visibility
All checks were successful
Build Docker / BuildImage (push) Successful in 2m55s
2025-10-26 21:00:12 +11:00
2979d3c4de feat: Update python version in docker
All checks were successful
Build Docker / BuildImage (push) Successful in 2m38s
2025-10-26 20:47:01 +11:00
a8b2c02164 feat: Add initial spotify widget
All checks were successful
Build Docker / BuildImage (push) Successful in 57s
2025-10-26 20:43:48 +11:00
372ba908b8 feat: Add tools to navbar
All checks were successful
Build Docker / BuildImage (push) Successful in 48s
2025-10-26 19:36:25 +11:00
1145b9205c Merge pull request 'Add initial ASCII art for curl connections' (#2) from feat/ascii_curl into main
All checks were successful
Build Docker / BuildImage (push) Successful in 50s
Reviewed-on: #2
2025-10-26 19:00:17 +11:00
a71c5b6663 feat: Update ascii templates to be nicer
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
2025-10-26 18:47:25 +11:00
724e800201 feat: Update curl template for index
All checks were successful
Build Docker / BuildImage (push) Successful in 53s
2025-10-26 18:43:25 +11:00
abcaa9283d feat: Add tools curl page 2025-10-26 18:43:25 +11:00
e175f68d25 feat: Add initial ascii art for curl connections 2025-10-26 18:43:25 +11:00
80b6a9bf46 feat: Update index page
All checks were successful
Build Docker / BuildImage (push) Successful in 57s
2025-10-26 18:42:22 +11:00
b089b8c0a8 feat: Add new tools api route
All checks were successful
Build Docker / BuildImage (push) Successful in 52s
2025-10-26 18:27:36 +11:00
8f774ba8f0 feat: Added tools page
All checks were successful
Build Docker / BuildImage (push) Successful in 2m9s
2025-10-26 18:00:18 +11:00
f4f5f47ee7 feat: Cleanup software blog style
All checks were successful
Build Docker / BuildImage (push) Successful in 2m41s
2025-10-24 16:10:28 +11:00
16f17a9486 feat: Add Software I use blog post
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
2025-10-23 15:00:05 +11:00
72483674f6 feat: Add now page for OCT
All checks were successful
Build Docker / BuildImage (push) Successful in 4m30s
2025-10-23 14:27:49 +11:00
b69c7f381b feat: Cleanup duplicate script code
All checks were successful
Build Docker / BuildImage (push) Successful in 59s
2025-10-16 17:37:48 +11:00
d7d4dbed8b feat: Add new status and ping route and update help menu
All checks were successful
Build Docker / BuildImage (push) Successful in 1m1s
2025-10-16 17:10:09 +11:00
2437b19836 feat: Add curl to container
All checks were successful
Build Docker / BuildImage (push) Successful in 4m33s
2025-10-16 16:57:41 +11:00
abd23e0eb8 fix: Add dateutil to requirements
All checks were successful
Build Docker / BuildImage (push) Successful in 2m59s
2025-10-16 16:54:16 +11:00
57a4b977ec feat: Add tool to estimate date of a webpage
All checks were successful
Build Docker / BuildImage (push) Successful in 2m34s
2025-10-16 16:48:26 +11:00
7f591e2724 fix: Cleanup blueprint names and add tests
All checks were successful
Build Docker / BuildImage (push) Successful in 2m17s
2025-10-13 15:32:31 +11:00
3d5c16f9cb fix: Verify legacy API redirects exist
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
This fixes an infinite redirect loop
2025-10-11 22:45:06 +11:00
fdb5f84c92 feat: Update config pulled from cloud 2025-10-11 22:32:29 +11:00
eaf363ee27 feat: Add curl support for blog pages
All checks were successful
Build Docker / BuildImage (push) Successful in 53s
2025-10-11 19:35:35 +11:00
0ea9db3473 feat: Update api routes to use similar json format to other routes 2025-10-11 19:16:50 +11:00
8d6acca5e9 feat: Add error message to header for HTML error responses 2025-10-11 18:55:24 +11:00
bfc1f0839a feat: Move getGitCommit from api to tools
All checks were successful
Build Docker / BuildImage (push) Successful in 1m4s
2025-10-11 18:51:22 +11:00
258061c64d feat: Use tools.json_response in hosting route
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
2025-10-11 18:14:20 +11:00
399ac5f0da feat: Move acme to blueprint and cleanup json responses
All checks were successful
Build Docker / BuildImage (push) Successful in 57s
2025-10-11 18:08:00 +11:00
74362de02a feat: Add better error messages to podcast routes
All checks were successful
Build Docker / BuildImage (push) Successful in 52s
2025-10-11 17:59:06 +11:00
9f7b93b8a1 feat: Move podcast routes to podcast blueprint
All checks were successful
Build Docker / BuildImage (push) Successful in 2m15s
2025-10-11 17:56:01 +11:00
665921d046 feat: Update about page info
All checks were successful
Build Docker / BuildImage (push) Successful in 58s
2025-10-11 17:47:03 +11:00
84cf772273 fix: Update index about section 2025-10-11 17:45:21 +11:00
22cd49a012 feat: Move error responses to new function in tools.py
All checks were successful
Build Docker / BuildImage (push) Successful in 1m6s
2025-10-11 17:39:46 +11:00
00d035a0e8 fix: Security issue in download route and cleanup
All checks were successful
Build Docker / BuildImage (push) Successful in 2m13s
2025-10-11 17:01:20 +11:00
fc56cafab8 feat: Split code into modules
All checks were successful
Build Docker / BuildImage (push) Successful in 4m7s
2025-10-10 22:58:06 +11:00
eee87e6ca7 fix: Cleanup code to follow linting 2025-10-10 21:35:47 +11:00
09852f19b6 feat: Move solana create transaction to new file
All checks were successful
Build Docker / BuildImage (push) Successful in 4m18s
2025-10-10 21:17:23 +11:00
8b464cd89d feat: Cleanup function names 2025-10-10 20:48:38 +11:00
98597768f3 feat: Update resume summary
All checks were successful
Build Docker / BuildImage (push) Successful in 2m31s
Make api endpoints more consistent
2025-10-09 16:04:07 +11:00
08f80ddb5c feat: Add some better checks
All checks were successful
Build Docker / BuildImage (push) Successful in 50s
2025-08-25 17:23:24 +10:00
33fd8136a7 feat: Add some more validation
All checks were successful
Build Docker / BuildImage (push) Successful in 1m2s
2025-08-24 21:26:14 +10:00
b2943bfeac feat: Add rate limits to exquiry
All checks were successful
Build Docker / BuildImage (push) Successful in 2m16s
2025-08-24 21:14:38 +10:00
c3c7c86a66 feat: Add now page for Aug 15
All checks were successful
Build Docker / BuildImage (push) Successful in 57s
2025-08-15 21:37:03 +10:00
8563a6857f feat: Update hosting to have presets
All checks were successful
Build Docker / BuildImage (push) Successful in 2m31s
2025-08-15 16:31:45 +10:00
1650d25d0f feat: Finish hosting enquiry page
All checks were successful
Build Docker / BuildImage (push) Successful in 2m12s
2025-08-14 16:01:46 +10:00
f4ee2297a7 feat: Add hosting page
All checks were successful
Build Docker / BuildImage (push) Successful in 6m14s
2025-08-13 12:46:43 +10:00
35ced02977 feat: Add new now page for July
All checks were successful
Build Docker / BuildImage (push) Successful in 4m4s
2025-07-21 11:15:36 +10:00
45709632d5 Merge branch 'feat/preloader'
All checks were successful
Build Docker / BuildImage (push) Successful in 1m13s
2025-07-03 14:19:46 +10:00
e65fe8cd30 fix: Load arg get returns None which causes exception
All checks were successful
Build Docker / BuildImage (push) Successful in 1m12s
2025-07-03 13:57:19 +10:00
21f725baf3 feat: Add option to force load page to show 2025-07-03 13:56:01 +10:00
c2a1995292 feat: Initial working preloader
All checks were successful
Build Docker / BuildImage (push) Successful in 1m1s
2025-07-03 13:37:51 +10:00
af93330bf5 feat: Preloader inital test 2025-07-03 13:17:38 +10:00
e43ae63d8a feat: Update resume dates
All checks were successful
Build Docker / BuildImage (push) Successful in 2m36s
2025-07-01 10:46:24 +10:00
add64ba889 fix: Update resume OG content
All checks were successful
Build Docker / BuildImage (push) Successful in 5m5s
2025-06-25 14:52:38 +10:00
8d13297cb0 feat: Update blog rendering
All checks were successful
Build Docker / BuildImage (push) Successful in 5m28s
2025-06-25 14:38:54 +10:00
eee95df47c feat: Redo some wording in the HNSDoH section
All checks were successful
Build Docker / BuildImage (push) Successful in 1m12s
2025-06-23 18:21:12 +10:00
1febeaf8a3 feat: Add HNSDoH to resume
All checks were successful
Build Docker / BuildImage (push) Successful in 1m17s
2025-06-23 18:17:59 +10:00
088be26b48 fix: Remove some unused fonts
All checks were successful
Build Docker / BuildImage (push) Successful in 1m17s
2025-06-23 17:47:12 +10:00
7f055f3607 fix: Shrink contact info on small screens
All checks were successful
Build Docker / BuildImage (push) Successful in 1m18s
2025-06-21 19:05:38 +10:00
5a455fcdca feat: Update index banner image
All checks were successful
Build Docker / BuildImage (push) Successful in 2m34s
2025-06-21 18:42:33 +10:00
846e28d92f fix: Update font
All checks were successful
Build Docker / BuildImage (push) Successful in 1m5s
2025-06-21 18:01:13 +10:00
ec3563093f feat: Add resume pdf route and fix some print formatting
All checks were successful
Build Docker / BuildImage (push) Successful in 1m11s
2025-06-21 17:54:06 +10:00
0ec96b4461 fix: Margin on print layout
All checks were successful
Build Docker / BuildImage (push) Successful in 2m21s
2025-06-21 15:56:58 +10:00
873504ecbf feat: Update resume page
Some checks failed
Build Docker / BuildImage (push) Has been cancelled
2025-06-21 15:55:42 +10:00
51f4a3462b feat: Add fallback version
All checks were successful
Build Docker / BuildImage (push) Successful in 1m6s
2025-06-20 22:20:42 +10:00
0656d8a95d feat: Add more api info
All checks were successful
Build Docker / BuildImage (push) Successful in 1m11s
2025-06-20 22:17:09 +10:00
a79c795672 feat: Add version to curl response
All checks were successful
Build Docker / BuildImage (push) Successful in 1m12s
2025-06-20 22:14:22 +10:00
159a40ecb5 feat: Cleanup unused prints
All checks were successful
Build Docker / BuildImage (push) Successful in 1m15s
2025-06-20 21:32:08 +10:00
9d359640d3 feat: Add FirePortal to sites
All checks were successful
Build Docker / BuildImage (push) Successful in 2m39s
2025-06-19 13:01:00 +10:00
f0c60a4cea feat: Add now page for Jun 19
All checks were successful
Build Docker / BuildImage (push) Successful in 2m44s
2025-06-19 12:53:32 +10:00
2c5ac7b475 feat: Refresh HNS address
All checks were successful
Build Docker / BuildImage (push) Successful in 2m21s
2025-06-04 14:40:44 +10:00
6a64de155d feat: Add new now page
All checks were successful
Build Docker / BuildImage (push) Successful in 2m7s
2025-05-28 12:05:02 +10:00
f5ba80fc16 feat: Update copyright date
All checks were successful
Build Docker / BuildImage (push) Successful in 40s
2025-04-22 16:53:30 +10:00
f8f48e9353 feat: Add Blog to navbar and write blog for Bobwallet + HNSAU nameserver
All checks were successful
Build Docker / BuildImage (push) Successful in 43s
2025-04-22 16:47:12 +10:00
f734e9ade4 fix: Use dynamic title for social embeds
All checks were successful
Build Docker / BuildImage (push) Successful in 43s
2025-04-21 22:20:43 +10:00
95e161aba9 feat: Update css for code blocks in blog
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
2025-04-21 22:13:59 +10:00
e4331e712a feat: Add blogs and updated some python dependencies
All checks were successful
Build Docker / BuildImage (push) Successful in 2m18s
2025-04-21 22:05:41 +10:00
4804016fdf feat: Add now page for March
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
2025-03-27 15:38:16 +11:00
ccb6cdb454 fix: Try to get correct IP from headers
All checks were successful
Build Docker / BuildImage (push) Successful in 2m10s
2025-02-21 11:04:53 +11:00
dade1f5d8d feat: Update podcast proxies
All checks were successful
Build Docker / BuildImage (push) Successful in 2m4s
2025-02-18 17:46:14 +11:00
cecfdea07f fix: Update now page to have a gap around text
All checks were successful
Build Docker / BuildImage (push) Successful in 44s
2025-01-30 14:39:42 +11:00
7bea502bdf feat: Add now page for Jan
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
2025-01-30 13:00:28 +11:00
1fd539bcb9 feat: Update sha265 for asssetlinks
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
2025-01-29 21:31:30 +11:00
f940f4418d feat: Add assetlinks.json
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2025-01-29 20:59:59 +11:00
de476eb7ab fix: Add more cached files to sw
All checks were successful
Build Docker / BuildImage (push) Successful in 50s
2025-01-29 20:53:46 +11:00
2fbf21eaaf feat: Update sw
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
2025-01-29 18:57:07 +11:00
8ccbe2ebf1 feat: Add service worker
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
2025-01-29 18:51:28 +11:00
d4136a396a feat: Update manifest
All checks were successful
Build Docker / BuildImage (push) Successful in 52s
2025-01-29 18:41:13 +11:00
622a4038ac feat: Add project api route
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2025-01-15 22:32:17 +11:00
a65896b80d feat: Add brand reveal to header
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
2025-01-11 17:57:23 +11:00
415703e21b fix: Update timing for brand reveal on index
All checks were successful
Build Docker / BuildImage (push) Successful in 1m59s
2025-01-11 17:36:31 +11:00
89bf5eecd7 feat: Add stWDBRN token and name reveal
All checks were successful
Build Docker / BuildImage (push) Successful in 47s
2025-01-06 16:26:37 +11:00
b4059910ec feat: Add fediverse creator tag
All checks were successful
Build Docker / BuildImage (push) Successful in 1m51s
2025-01-05 21:51:56 +11:00
7896f71534 fix: Update cleanSite hook to clean now page html files
All checks were successful
Build Docker / BuildImage (push) Successful in 1m9s
2025-01-04 20:11:35 +11:00
f9b30a3fe6 fix: Update sitemap to have 2024 now page
All checks were successful
Build Docker / BuildImage (push) Successful in 36s
2025-01-01 01:30:25 +11:00
31c80276de feat: Add 2024 overview now page
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
2025-01-01 01:28:16 +11:00
840ba4c10c feat: Add log message for ACME requests
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
2024-12-30 13:33:58 +11:00
53bf28b208 fix: Update cloudflare version
All checks were successful
Build Docker / BuildImage (push) Successful in 1m47s
2024-12-30 10:56:20 +11:00
d651e3a20c feat: Add new BTC address and fix file type of proofs
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
2024-12-29 22:44:00 +11:00
fb376a4906 feat: Update HNS address
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2024-12-26 18:49:36 +11:00
cd69d86246 feat: Add new now page
All checks were successful
Build Docker / BuildImage (push) Successful in 1m48s
2024-12-19 12:33:56 +11:00
1ae1eeb159 feat: Update stWDBRN name
All checks were successful
Build Docker / BuildImage (push) Successful in 1m51s
2024-12-05 15:58:41 +11:00
5171fffab5 feat: Add stWDBRN metadata
All checks were successful
Build Docker / BuildImage (push) Successful in 44s
2024-12-04 15:57:25 +11:00
ca1e99013d fix: Update stWDBRN image size
All checks were successful
Build Docker / BuildImage (push) Successful in 41s
2024-12-04 15:54:28 +11:00
7c71c994f2 feat: Add stWDBRN firewallet logo
All checks were successful
Build Docker / BuildImage (push) Successful in 1m42s
2024-12-04 15:51:50 +11:00
70655a4d39 feat: Update print layout to make resume display nicer
All checks were successful
Build Docker / BuildImage (push) Successful in 32s
2024-12-03 14:00:39 +11:00
1551799b88 fix: Lock pydantic to fix cloudflare errors
All checks were successful
Build Docker / BuildImage (push) Successful in 1m34s
2024-12-03 13:32:47 +11:00
4ab0db973f feat: Update print css for resume
All checks were successful
Build Docker / BuildImage (push) Successful in 1m57s
2024-12-03 12:23:55 +11:00
a3a2748d15 feat: Add PYUSD SOL and ETH tokens
All checks were successful
Build Docker / BuildImage (push) Successful in 40s
2024-12-02 12:03:42 +11:00
ce8897d578 feat: Add solana action version and chain headers
All checks were successful
Build Docker / BuildImage (push) Successful in 1m40s
2024-11-19 15:18:10 +11:00
637562f920 feat: Add rss endpoints
All checks were successful
Build Docker / BuildImage (push) Successful in 1m42s
2024-11-12 14:08:30 +11:00
42aff1f455 feat: Add api routes for email and other basic info
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
2024-10-31 21:11:13 +11:00
855f6b3c99 feat: Update BTC lightning address
All checks were successful
Build Docker / BuildImage (push) Successful in 1m46s
2024-10-24 10:52:00 +11:00
8a50ae7e32 feat: Add latest now page
All checks were successful
Build Docker / BuildImage (push) Successful in 1m35s
2024-10-22 17:41:06 +11:00
1a3572e64c feat: Add latest now page
All checks were successful
Build Docker / BuildImage (push) Successful in 1m20s
2024-10-15 17:48:24 +11:00
ad6e3fe9d8 fix: Update qr code function
All checks were successful
Build Docker / BuildImage (push) Successful in 1m32s
2024-10-09 16:50:40 +11:00
99b63592d0 fix: Add null check for headers
All checks were successful
Build Docker / BuildImage (push) Successful in 28s
2024-10-08 19:03:02 +11:00
6c004d14bd feat: Add now json endpoint
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
2024-10-07 18:03:04 +11:00
a6b488d1a6 fix: Update now.rss to include better descriptions
All checks were successful
Build Docker / BuildImage (push) Successful in 40s
2024-10-07 17:44:42 +11:00
0cef259ecc feat: Add now page rss
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2024-10-07 17:39:25 +11:00
150bc17ed4 feat: Add Handypedia updates to latest now page
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
2024-10-07 17:22:58 +11:00
c9e9c9be46 feat: Add new now page for 7_10_24
All checks were successful
Build Docker / BuildImage (push) Successful in 1m13s
2024-10-07 17:17:20 +11:00
90d8dc3428 feat: Add now to sitemap 2024-10-07 17:04:54 +11:00
863d11cffd fix: Add route for favicon.ico 2024-10-05 20:04:38 +10:00
b3d965d220 feat: Update sitemap and WVAC podcast link
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
2024-10-02 12:23:57 +10:00
496205c6b4 feat: Add download option for PGP public key
All checks were successful
Build Docker / BuildImage (push) Successful in 43s
2024-09-30 16:19:49 +10:00
9552df4b4e feat: Add now page for 27_09
All checks were successful
Build Docker / BuildImage (push) Successful in 1m35s
2024-09-27 13:34:26 +10:00
9881584cdb fix: Update check for project images to correctly display
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2024-09-11 21:03:11 +10:00
867583d30c feat: Update resume
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2024-09-11 20:57:10 +10:00
125768b01e feat: Update manifest colour
All checks were successful
Build Docker / BuildImage (push) Successful in 1m24s
2024-09-11 17:25:55 +10:00
320538ad17 fix: Set the now image to have a max width of 95%
All checks were successful
Build Docker / BuildImage (push) Successful in 1m38s
2024-09-11 17:17:46 +10:00
56a5539106 feat: Add Now page 7 Sep
All checks were successful
Build Docker / BuildImage (push) Successful in 1m41s
2024-09-07 16:14:17 +10:00
56b1048622 feat: Add default nostr name and fix loading js
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2024-09-03 14:05:42 +10:00
3e4d5c2633 fix: Use header.get instead of user agent
All checks were successful
Build Docker / BuildImage (push) Successful in 44s
2024-09-02 19:56:57 +10:00
cdb7eb86a5 feat: Check if user_agent is a crawler before showing loading page
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2024-09-02 19:49:32 +10:00
6de2518f1f feat: Add APT, BCH, DASH and TRX coins
All checks were successful
Build Docker / BuildImage (push) Successful in 47s
2024-08-27 10:21:40 +10:00
34580eeab4 feat: Remove unused nft data
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
2024-08-24 10:51:53 +10:00
0dd03e544c feat: Add profile nft metadata
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
2024-08-24 10:24:20 +10:00
84a6310189 feat: Add Woodburn ico favicon
All checks were successful
Build Docker / BuildImage (push) Successful in 47s
2024-08-21 21:35:11 +10:00
68f8c55817 feat: Add option to get custom message from NC config
All checks were successful
Build Docker / BuildImage (push) Successful in 44s
2024-08-21 17:42:31 +10:00
9c0d592a24 feat: Add searching for files before returning a 404
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
2024-08-21 11:49:03 +10:00
56349561f9 feat: Add better error messages on crypto selector
All checks were successful
Build Docker / BuildImage (push) Successful in 43s
2024-08-20 16:38:11 +10:00
702282d11f feat: Add Noble to wallet addresses
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
2024-08-20 16:23:16 +10:00
46856a9399 fix: Update cloudflare to use new API
All checks were successful
Build Docker / BuildImage (push) Successful in 41s
2024-08-20 13:02:45 +10:00
eecf9b8db8 fix: Update TON address to use Keystone address
All checks were successful
Build Docker / BuildImage (push) Successful in 38s
2024-08-20 10:33:02 +10:00
1c891d971f feat: Update timing for profile hover blur
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
2024-08-15 18:53:15 +10:00
6dfa664807 feat: Update the style of the clock to be more readable
All checks were successful
Build Docker / BuildImage (push) Successful in 43s
2024-08-15 18:37:02 +10:00
5a27be5a3c feat: Add meeting link
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
Note this link is not fully integrated with calendar yet
2024-08-15 14:47:30 +10:00
8687daecfd fix: Update tooltip to say copied when you copy a crypto address
All checks were successful
Build Docker / BuildImage (push) Successful in 46s
2024-08-15 14:29:36 +10:00
6b5fe4bc2f fix: Don't show loading animation on mobiles
All checks were successful
Build Docker / BuildImage (push) Successful in 53s
2024-08-15 14:18:26 +10:00
c86bae36ad fix: Remove overflow on projects page
All checks were successful
Build Docker / BuildImage (push) Successful in 1m35s
2024-08-15 14:12:00 +10:00
ef40e8078c fix: Update social icons to work on small mobiles
All checks were successful
Build Docker / BuildImage (push) Successful in 59s
2024-08-15 14:02:35 +10:00
80fc1cdc4d feat: Add clock to home page
All checks were successful
Build Docker / BuildImage (push) Successful in 47s
2024-08-15 13:43:35 +10:00
331de40dfd fix: Update address max width to stop overflow on mobile
All checks were successful
Build Docker / BuildImage (push) Successful in 43s
2024-08-07 09:57:23 +10:00
f3ee6607e7 feat: Add now page for 5_8
All checks were successful
Build Docker / BuildImage (push) Successful in 1m45s
2024-08-05 14:27:53 +10:00
eec66b13ca feat: Add crypto address verifications
All checks were successful
Build Docker / BuildImage (push) Successful in 35s
2024-07-18 14:30:12 +10:00
9fc218feb1 feat: Add now page
All checks were successful
Build Docker / BuildImage (push) Successful in 4m37s
2024-07-15 12:16:56 +10:00
9ed68d1f0b fix: Manually add cors options header
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
2024-07-10 19:52:43 +10:00
80cc8022eb feat: Add options using flask cors
All checks were successful
Build Docker / BuildImage (push) Successful in 40s
2024-07-10 19:20:27 +10:00
6bf45b9c2b fix: Add headers to options response
All checks were successful
Build Docker / BuildImage (push) Successful in 38s
2024-07-10 18:03:43 +10:00
860d070c55 feat: Add options route
All checks were successful
Build Docker / BuildImage (push) Successful in 44s
2024-07-10 17:59:47 +10:00
7e49d23736 fix: Add versions to requirements
All checks were successful
Build Docker / BuildImage (push) Successful in 1m12s
2024-07-03 15:24:41 +10:00
69fb34c2ba fix: Update to newest version of solana
All checks were successful
Build Docker / BuildImage (push) Successful in 34s
2024-07-03 15:07:27 +10:00
6664de6c07 feat: Add solana blinks
All checks were successful
Build Docker / BuildImage (push) Successful in 1m36s
2024-07-03 14:59:30 +10:00
b93515ff32 feat: Add XRP toml
All checks were successful
Build Docker / BuildImage (push) Successful in 34s
2024-07-02 13:48:16 +10:00
230042cc7b fix: Update dependencies
All checks were successful
Build Docker / BuildImage (push) Successful in 35s
2024-07-01 13:17:50 +10:00
98acc8543c fix: Try to fix error in requirements
All checks were successful
Build Docker / BuildImage (push) Successful in 1m11s
2024-07-01 13:05:20 +10:00
eea21cea1e fix: Don't show loader to google crawler
All checks were successful
Build Docker / BuildImage (push) Successful in 1m10s
2024-07-01 11:37:19 +10:00
fb8136f3b6 feat: Update default coins
All checks were successful
Build Docker / BuildImage (push) Successful in 34s
2024-06-27 12:11:02 +10:00
e84c39030d feat: Add XLM and update ADA address to use standard derivation
All checks were successful
Build Docker / BuildImage (push) Successful in 40s
2024-06-25 21:45:21 +10:00
75308cb264 feat: Add now page
All checks were successful
Build Docker / BuildImage (push) Successful in 45s
2024-06-24 13:47:11 +10:00
9b81a0bf18 fix: Added missing HNS assets for Handypedia and other HNS sites
All checks were successful
Build Docker / BuildImage (push) Successful in 49s
2024-06-20 17:22:42 +10:00
005a306fc6 fix: Update reverse proxy causing manifest breaking
All checks were successful
Build Docker / BuildImage (push) Successful in 36s
2024-06-19 21:23:52 +10:00
cb13ca0a3b feat: Add Kasper coin 2024-06-19 16:58:35 +10:00
5685830cba fix: Cleanup index variables
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
2024-06-19 13:16:30 +10:00
1f9b38306c fix: Index page 2024-06-19 13:12:45 +10:00
9568cfe177 fix: Use global cache for projects list
All checks were successful
Build Docker / BuildImage (push) Successful in 31s
2024-06-19 12:50:48 +10:00
18619efe39 feat: Add recent projects to index and updated projects page
All checks were successful
Build Docker / BuildImage (push) Successful in 37s
2024-06-18 12:44:26 +10:00
719221d74f feat: Remove onion link
All checks were successful
Build Docker / BuildImage (push) Successful in 32s
2024-06-17 22:05:41 +10:00
5d95307ae2 fix: Manifest should use host automatically 2024-06-17 22:04:12 +10:00
1b017d919a feat: Don't show loading screen to redirect from icann to hns
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
2024-06-17 21:51:32 +10:00
ce5ec9aace feat: Test for referrers
All checks were successful
Build Docker / BuildImage (push) Successful in 56s
2024-06-17 21:47:44 +10:00
668dc8683b Merge branch 'develop'
All checks were successful
Build Docker / BuildImage (push) Successful in 32s
2024-06-17 21:30:33 +10:00
26c91c030a Merge branch 'release/v1.0' into main
All checks were successful
Build Docker / Build Image (push) Successful in 29s
2023-11-03 14:15:55 +11:00
164 changed files with 8914 additions and 1221 deletions

33
.dockerignore Normal file
View File

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

View File

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

3
.gitignore vendored
View File

@@ -3,3 +3,6 @@ __pycache__/
.env .env
.vs/ .vs/
.venv/
*.tmp
testing/

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

View File

@@ -0,0 +1,8 @@
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "au.woodburn.nathan",
"sha256_cert_fingerprints": ["D8:97:22:C4:C5:AA:AC:6D:7B:57:F0:19:FF:A1:E7:2A:92:71:EE:CF:1F:E1:AF:5A:87:22:0D:00:76:9D:83:80"]
}
}]

View File

@@ -0,0 +1,6 @@
I hereby confirm that I am the owner of the Bitcoin address bc1qzsp3wc2nwayl8awun57tggjn92w0awp9mzsn90.
Nathan.Woodburn/
--------------------
HzEsSV17nmbMwnz55sW6jW/AnmYxW0TAdgiJsYRUyWs7WjBaGlzrvXkH1R8qosQXSvQ1nZNe9dS5SUCZtbNZzuM=
--------------------
You can verify this signature by pasting it into a signature verification tool such as https://www.verifybitcoinmessage.com/. Don't forget to remove trailing newlines.

View File

@@ -0,0 +1,6 @@
I hereby confirm that I am the owner of the EVM address 0x6cB4B39bEc23a921C9a20D061Bf17d4640B0d39e.
Nathan.Woodburn/
--------------------
0x254919e1f2035a4f04614da9e1fbc1f45dab31b03b0baf1bb3325a9f9e437f1f787b99ebc6716b822fc190284c2c678054c91835492ff0df239ec60f6166587f1c
--------------------
You can verify this signature by pasting it into a signature verification tool such as https://etherscan.io/verifiedSignatures

View File

@@ -0,0 +1,13 @@
I hereby confirm that I am the owner of the SOL address AJsPEEe6S7XSiVcdZKbeV8GRp1QuhFUsG8mLrqL4XgiU.
Nathan.Woodburn/
--------------------
[71,63,207,190,90,17,145,39,4,98,110,176,86,140,143,107,237,96,24,43,2,116,21,70,47,98,192,24,193,210,89,220,30,128,219,105,9,35,146,188,216,143,164,32,255,44,146,249,153,33,54,214,203,159,80,26,107,165,217,240,153,61,39,0]
--------------------
0x473fcfbe5a11912704626eb0568c8f6bed60182b027415462f62c018c1d259dc1e80db69092392bcd88fa420ff2c92f9992136d6cb9f501a6ba5d9f0993d2700
--------------------
2Rd2EkAUwC8u4DtCZ5BXTkJEvWxozrxmcEzn7VbJFFbL81YLQngH9V1bTu3vivaQz7ZGqs5YtpPWxomsYeE7Ws6F
--------------------
Rz/PvloRkScEYm6wVoyPa+1gGCsCdBVGL2LAGMHSWdwegNtpCSOSvNiPpCD/LJL5mSE21sufUBprpdnwmT0nAA==
--------------------
You can verify this signature by pasting it into a signature verification tool such as https://amacar.github.io/solana-tools/#verify-message
Please note I have included various formats for the signature to make it easier to verify.

View File

@@ -23,5 +23,10 @@
"TON": "Toncoin (TON)", "TON": "Toncoin (TON)",
"OP": "Optimism (OP)", "OP": "Optimism (OP)",
"IAA": "IRIS (IAA)", "IAA": "IRIS (IAA)",
"NEAR": "NEAR Protocol (NEAR)" "NEAR": "NEAR Protocol (NEAR)",
"KAS": "Kasper (KAS)",
"XLM": "Stellar (XLM)",
"APT": "Aptos (APT)",
"TRX": "Tron (TRX)",
"BCH": "Bitcoin Cash (BCH)"
} }

View File

@@ -1,6 +1,6 @@
{ {
"ETH":"woodburn.au", "ETH":"woodburn.au",
"HNS":"woodburn", "HNS":"nathan.woodburn",
"SOL":"woodburn.sol", "SOL":"woodburn.sol",
"ADA": "$nathanwoodburn", "ADA": "$nathanwoodburn",
"MATIC": "woodburn.au", "MATIC": "woodburn.au",

View File

@@ -19,6 +19,21 @@
"name": "USDC", "name": "USDC",
"chain": "SOL" "chain": "SOL"
}, },
{
"symbol": "USDC",
"name": "USDC",
"chain": "NOBLE"
},
{
"symbol": "PYUSD",
"name": "PayPal USD",
"chain": "ETH"
},
{
"symbol": "PYUSD",
"name": "PayPal USD",
"chain": "SOL"
},
{ {
"symbol": "WDBRN", "symbol": "WDBRN",
"name": "Woodburn", "name": "Woodburn",
@@ -63,6 +78,11 @@
"symbol": "BTC", "symbol": "BTC",
"name": "Bitcoin Lightning", "name": "Bitcoin Lightning",
"chain": "null", "chain": "null",
"address": "hushedmercury55@walletofsatoshi.com" "address": "thinbadger6@primal.net"
},
{
"symbol": "stWDBRN",
"name": "Woodburn Vault",
"chain": "SOL"
} }
] ]

View File

@@ -1 +1 @@
addr1qyudjff2lfuxjjjfz0y542drw4srxq2xxtgrq22vm8ymvwwagq028esaz4rsexct8zzsuds29e03eaykahlvhdx0k9es4gnk27 addr1qy5l7vmx9l2uexv44hzjak4zmwecee4hht0k6shtk5jh7cjzu99z8vx5n467fquzradx7p42grdylv3zq2cgfw0f32fs443hxs

1
.well-known/wallets/APT Normal file
View File

@@ -0,0 +1 @@
0x372b3c513d149e5511912eba22e31f07d2b289e20ba84b2e0b7756e7a00295c3

1
.well-known/wallets/BCH Normal file
View File

@@ -0,0 +1 @@
qpsgs9daa6e2mn4v0u02pfunsme68a5uayn7e8knug

View File

@@ -1 +1 @@
bc1qhs94zzcw64qnwq4hvk056rwxwvgrkd7tq7d4xw bc1qzsp3wc2nwayl8awun57tggjn92w0awp9mzsn90

1
.well-known/wallets/DASH Normal file
View File

@@ -0,0 +1 @@
Xpr5auWs1waBmWT3XsWXwzu8Di32x8VfH2

View File

@@ -1 +1 @@
hs1qk4sq6mk3kcshp02xgchukv09m38czdnq5qv76w hs1qh7uzytf2ftwkd9dmjjs7az9qfver5m7dd7x4ej

1
.well-known/wallets/KAS Normal file
View File

@@ -0,0 +1 @@
kaspa:qzl7av7gq5j594pcs2gn6zf2xadpmhdm90nygjstvte0n6gt9f4fgx0w2dhm8

View File

@@ -0,0 +1 @@
noble1ugraczuyfmxy8k38nps4fu7e5derryzxywjul2

View File

@@ -1 +1 @@
UQAtpYb-yOaTSA40mJRvUxKv1WVeIPi-g_4Jqn0oWYypsQIf UQDqC1B0a3S9th8ncaQHYJ689dnu9c0zJXeV727UMak9WbBm

1
.well-known/wallets/TRX Normal file
View File

@@ -0,0 +1 @@
THjwavxGZahj1scVw75fhGP2HCAcjNxwsK

1
.well-known/wallets/XLM Normal file
View File

@@ -0,0 +1 @@
GCK4PA53V26MNP6U57EPK7EA42TBQMGJ4TUPMUPLLQNPZ64YX3XVLZGQ

View File

@@ -0,0 +1,12 @@
[METADATA]
modified = 2024-07-02T00:00:00.000Z
expires = 2050-07-02T00:00:00.000Z
[[ACCOUNTS]]
address = "rKzdnYvwDyeki5VCgMwjuofjBjAbg3DJnB"
desc = "Nathan.Woodburn/ (Twitter: @nathanwoodburn)"
[[PRINCIPALS]]
name = "Nathan Woodburn"
email = "xrp@nathan.woodburn.au"
social_1 = "https://nathan.woodburn.au"

View File

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

BIN
NathanWoodburn.bsdesign Normal file

Binary file not shown.

35
addCoin.py Normal file
View File

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

36
blueprints/acme.py Normal file
View File

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

324
blueprints/api.py Normal file
View File

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

164
blueprints/blog.py Normal file
View File

@@ -0,0 +1,164 @@
import os
from flask import Blueprint, render_template, request, jsonify
import markdown
from bs4 import BeautifulSoup
import re
from tools import isCLI, getClientIP, getHandshakeScript
app = Blueprint('blog', __name__, url_prefix='/blog')
def list_page_files():
blog_pages = os.listdir("data/blog")
# Sort pages by modified time, newest first
blog_pages.sort(
key=lambda x: os.path.getmtime(os.path.join("data/blog", x)), reverse=True)
# Remove .md extension
blog_pages = [page.removesuffix(".md")
for page in blog_pages if page.endswith(".md")]
return blog_pages
def render_page(date, handshake_scripts=None):
# Convert md to html
if not os.path.exists(f"data/blog/{date}.md"):
return render_template("404.html"), 404
with open(f"data/blog/{date}.md", "r") as f:
content = f.read()
# Get the title from the file name
title = date.removesuffix(".md").replace("_", " ")
# Convert the md to html
content = markdown.markdown(
content, extensions=['sane_lists', 'codehilite', 'fenced_code'])
# Add target="_blank" to all links
content = content.replace('<a href="', '<a target="_blank" href="')
content = content.replace("<h4", "<h4 style='margin-bottom:0px;'")
content = fix_numbered_lists(content)
return render_template(
"blog/template.html",
title=title,
content=content,
handshake_scripts=handshake_scripts,
)
def fix_numbered_lists(html):
soup = BeautifulSoup(html, 'html.parser')
# Find the <p> tag containing numbered steps
paragraphs = soup.find_all('p')
for p in paragraphs:
content = p.decode_contents() # type: ignore
# Check for likely numbered step structure
if re.search(r'1\.\s', content):
# Split into pre-list and numbered steps
# Match: <br>, optional whitespace, then a number and dot
parts = re.split(r'(?:<br\s*/?>)?\s*(\d+)\.\s', content)
# Result: [pre-text, '1', step1, '2', step2, ..., '10', step10]
pre_text = parts[0].strip()
steps = parts[1:]
# Assemble the ordered list
ol_items = []
for i in range(0, len(steps), 2):
if i+1 < len(steps):
step_html = steps[i+1].strip()
ol_items.append(
f"<li style='list-style: auto;'>{step_html}</li>")
# Build the final list HTML
ol_html = "<ol>\n" + "\n".join(ol_items) + "\n</ol>"
# Rebuild paragraph with optional pre-text
new_html = f"{pre_text}<br />\n{ol_html}" if pre_text else ol_html
# Replace old <p> with parsed version
new_fragment = BeautifulSoup(new_html, 'html.parser')
p.replace_with(new_fragment)
break # Only process the first matching <p>
return str(soup)
def render_home(handshake_scripts: str | None = None):
# Get a list of pages
blog_pages = list_page_files()
# Create a html list of pages
blog_pages = [
f"""<li class="list-group-item">
<p style="margin-bottom: 0px;"><a href='/blog/{page}'>{page.replace("_", " ")}</a></p>
</li>"""
for page in blog_pages
]
# Join the list
blog_pages = "\n".join(blog_pages)
# Render the template
return render_template(
"blog/blog.html",
blogs=blog_pages,
handshake_scripts=handshake_scripts,
)
@app.route("/", strict_slashes=False)
def index():
if not isCLI(request):
return render_home(handshake_scripts=getHandshakeScript(request.host))
# Get a list of pages
blog_pages = list_page_files()
# Create a html list of pages
blog_pages = [
{"name": page.replace("_", " "), "url": f"/blog/{page}", "download": f"/blog/{page}.md"} for page in blog_pages
]
# Render the template
return jsonify({
"status": 200,
"message": "Check out my various blog postsa",
"ip": getClientIP(request),
"blogs": blog_pages
}), 200
@app.route("/<path:path>")
def path(path):
if not isCLI(request):
return render_page(path, handshake_scripts=getHandshakeScript(request.host))
# Convert md to html
if not os.path.exists(f"data/blog/{path}.md"):
return render_template("404.html"), 404
with open(f"data/blog/{path}.md", "r") as f:
content = f.read()
# Get the title from the file name
title = path.replace("_", " ")
return jsonify({
"status": 200,
"message": f"Blog post: {title}",
"ip": getClientIP(request),
"title": title,
"content": content,
"download": f"/blog/{path}.md"
}), 200
@app.route("/<path:path>.md")
def path_md(path):
if not os.path.exists(f"data/blog/{path}.md"):
return render_template("404.html"), 404
with open(f"data/blog/{path}.md", "r") as f:
content = f.read()
# Return the raw markdown file
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}

201
blueprints/now.py Normal file
View File

@@ -0,0 +1,201 @@
from flask import Blueprint, render_template, make_response, request, jsonify
import datetime
import os
from tools import getHandshakeScript, error_response, isCLI
from curl import get_header, MAX_WIDTH
from bs4 import BeautifulSoup
import re
# Create blueprint
app = Blueprint('now', __name__, url_prefix='/now')
def list_page_files():
now_pages = os.listdir("templates/now")
now_pages = [
page for page in now_pages if page != "template.html" and page != "old.html"
]
now_pages.sort(reverse=True)
return now_pages
def list_dates():
now_pages = list_page_files()
now_dates = [page.split(".")[0] for page in now_pages]
return now_dates
def get_latest_date(formatted=False):
if formatted:
date = list_dates()[0]
date = datetime.datetime.strptime(date, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y")
return date
return list_dates()[0]
def render_latest(handshake_scripts=None):
now_page = list_dates()[0]
return render(now_page, handshake_scripts=handshake_scripts)
def render(date, handshake_scripts=None):
# If the date is not available, render the latest page
if date is None:
return render_latest(handshake_scripts=handshake_scripts)
# Remove .html
date = date.removesuffix(".html")
if date not in list_dates():
return error_response(request)
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
return render_template(f"now/{date}.html", DATE=date_formatted, handshake_scripts=handshake_scripts)
def render_curl(date=None):
# If the date is not available, render the latest page
if date is None:
date = get_latest_date()
# Remove .html if present
date = date.removesuffix(".html")
if date not in list_dates():
return error_response(request)
# Format the date nicely
date_formatted = datetime.datetime.strptime(date, "%y_%m_%d")
date_formatted = date_formatted.strftime("%A, %B %d, %Y")
# Load HTML
with open(f"templates/now/{date}.html", "r", encoding="utf-8") as f:
raw_html = f.read().replace("{{ date }}", date_formatted)
soup = BeautifulSoup(raw_html, 'html.parser')
posts = []
# Find divs matching your pattern
divs = soup.find_all("div", style=re.compile(r"max-width:\s*700px", re.IGNORECASE))
if not divs:
return error_response(request, message="No content found for CLI rendering.")
for div in divs:
# header could be h1/h2/h3 inside the div
header_tag = div.find(["h1", "h2", "h3"]) # type: ignore
# content is usually one or more <p> tags inside the div
p_tags = div.find_all("p") # type: ignore
if header_tag and p_tags:
header_text = header_tag.get_text(strip=True) # type: ignore
content_lines = []
for p in p_tags:
# Extract text
text = p.get_text(strip=False)
# Extract any <a> links in the paragraph
links = [a.get("href") for a in p.find_all("a", href=True)] # type: ignore
# Set max width for text wrapping
# Wrap text manually
wrapped_lines = []
for line in text.splitlines():
while len(line) > MAX_WIDTH:
# Find last space within max_width
split_at = line.rfind(' ', 0, MAX_WIDTH)
if split_at == -1:
split_at = MAX_WIDTH
wrapped_lines.append(line[:split_at].rstrip())
line = line[split_at:].lstrip()
wrapped_lines.append(line)
text = "\n".join(wrapped_lines)
if links:
text += "\nLinks: " + ", ".join(links) # type: ignore
content_lines.append(text)
content_text = "\n\n".join(content_lines)
posts.append({"header": header_text, "content": content_text})
# Build final response
response = ""
for post in posts:
response += f"{post['header']}\n\n{post['content']}\n\n"
return render_template("now.ascii", date=date_formatted, content=response, header=get_header())
@app.route("/", strict_slashes=False)
def index():
if isCLI(request):
return render_curl()
return render_latest(handshake_scripts=getHandshakeScript(request.host))
@app.route("/<path:path>")
def path(path):
if isCLI(request):
return render_curl(path)
return render(path, handshake_scripts=getHandshakeScript(request.host))
@app.route("/old", strict_slashes=False)
def old():
now_dates = list_dates()[1:]
if isCLI(request):
response = ""
for date in now_dates:
link = date
date_fmt = datetime.datetime.strptime(date, "%y_%m_%d")
date_fmt = date_fmt.strftime("%A, %B %d, %Y")
response += f"{date_fmt} - /now/{link}\n"
return render_template("now.ascii", date="Old Now Pages", content=response, header=get_header())
html = '<ul class="list-group">'
html += f'<a style="text-decoration:none;" href="/now"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{get_latest_date(True)}</li></a>'
for date in now_dates:
link = date
date = datetime.datetime.strptime(date, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y")
html += f'<a style="text-decoration:none;" href="/now/{link}"><li class="list-group-item" style="background-color:#000000;color:#ffffff;">{date}</li></a>'
html += "</ul>"
return render_template(
"now/old.html", handshake_scripts=getHandshakeScript(request.host), now_pages=html
)
@app.route("/now.rss")
@app.route("/now.xml")
@app.route("/rss.xml")
def rss():
host = "https://" + request.host
if ":" in request.host:
host = "http://" + request.host
# Generate RSS feed
now_pages = list_page_files()
rss = f'<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Nathan.Woodburn/</title><link>{host}</link><description>See what I\'ve been up to</description><language>en-us</language><lastBuildDate>{datetime.datetime.now(tz=datetime.timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")}</lastBuildDate><atom:link href="{host}/now.rss" rel="self" type="application/rss+xml" />'
for page in now_pages:
link = page.strip(".html")
date = datetime.datetime.strptime(link, "%y_%m_%d")
date = date.strftime("%A, %B %d, %Y")
rss += f'<item><title>What\'s Happening {date}</title><link>{host}/now/{link}</link><description>Latest updates for {date}</description><guid>{host}/now/{link}</guid></item>'
rss += "</channel></rss>"
return make_response(rss, 200, {"Content-Type": "application/rss+xml"})
@app.route("/now.json")
def json():
now_pages = list_page_files()
host = "https://" + request.host
if ":" in request.host:
host = "http://" + request.host
now_pages = [{"url": host+"/now/"+page.strip(".html"), "date": datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime(
"%A, %B %d, %Y"), "title": "What's Happening "+datetime.datetime.strptime(page.strip(".html"), "%y_%m_%d").strftime("%A, %B %d, %Y")} for page in now_pages]
return jsonify(now_pages)

59
blueprints/podcast.py Normal file
View File

@@ -0,0 +1,59 @@
from flask import Blueprint, make_response, request
from tools import error_response
import requests
app = Blueprint('podcast', __name__)
@app.route("/ID1")
def index():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1")
if req.status_code != 200:
return error_response(request, "Error from Podcast Server", req.status_code)
return make_response(
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
)
@app.route("/ID1/")
def contents():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1/")
if req.status_code != 200:
return error_response(request, "Error from Podcast Server", req.status_code)
return make_response(
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
)
@app.route("/ID1/<path:path>")
def path(path):
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1/" + path)
if req.status_code != 200:
return error_response(request, "Error from Podcast Server", req.status_code)
return make_response(
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
)
@app.route("/ID1.xml")
def xml():
# Proxy to ID1 url
req = requests.get("https://podcasts.c.woodburn.au/ID1.xml")
if req.status_code != 200:
return error_response(request, "Error from Podcast Server", req.status_code)
return make_response(
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
)
@app.route("/podsync.opml")
def podsync():
req = requests.get("https://podcasts.c.woodburn.au/podsync.opml")
if req.status_code != 200:
return error_response(request, "Error from Podcast Server", req.status_code)
return make_response(
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
)

125
blueprints/sol.py Normal file
View File

@@ -0,0 +1,125 @@
from flask import Blueprint, request, jsonify, make_response
from solders.pubkey import Pubkey
from solana.rpc.api import Client
from solders.system_program import TransferParams, transfer
from solders.message import MessageV0
from solders.transaction import VersionedTransaction
from solders.null_signer import NullSigner
import binascii
import base64
import os
app = Blueprint('sol', __name__)
SOLANA_HEADERS = {
"Content-Type": "application/json",
"X-Action-Version": "2.4.2",
"X-Blockchain-Ids": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
}
SOLANA_ADDRESS = None
if os.path.isfile(".well-known/wallets/SOL"):
with open(".well-known/wallets/SOL") as file:
address = file.read()
SOLANA_ADDRESS = Pubkey.from_string(address.strip())
def create_transaction(sender_address: str, amount: float) -> str:
if SOLANA_ADDRESS is None:
raise ValueError("SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address.")
# Create transaction
sender = Pubkey.from_string(sender_address)
transfer_ix = transfer(
TransferParams(
from_pubkey=sender, to_pubkey=SOLANA_ADDRESS, lamports=int(
amount * 1000000000)
)
)
solana_client = Client("https://api.mainnet-beta.solana.com")
blockhashData = solana_client.get_latest_blockhash()
blockhash = blockhashData.value.blockhash
msg = MessageV0.try_compile(
payer=sender,
instructions=[transfer_ix],
address_lookup_table_accounts=[],
recent_blockhash=blockhash,
)
tx = VersionedTransaction(message=msg, keypairs=[NullSigner(sender)])
tx = bytes(tx).hex()
raw_bytes = binascii.unhexlify(tx)
base64_string = base64.b64encode(raw_bytes).decode("utf-8")
return base64_string
def get_solana_address() -> str:
if SOLANA_ADDRESS is None:
raise ValueError("SOLANA_ADDRESS is not set. Please ensure the .well-known/wallets/SOL file exists and contains a valid address.")
return str(SOLANA_ADDRESS)
@app.route("/donate", methods=["GET", "OPTIONS"])
def sol_donate():
data = {
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
"label": "Donate to Nathan.Woodburn/",
"title": "Donate to Nathan.Woodburn/",
"description": "Student, developer, and crypto enthusiast",
"links": {
"actions": [
{"label": "0.01 SOL", "href": "/api/v1/donate/0.01"},
{"label": "0.1 SOL", "href": "/api/v1/donate/0.1"},
{"label": "1 SOL", "href": "/api/v1/donate/1"},
{
"href": "/api/v1/donate/{amount}",
"label": "Donate",
"parameters": [
{"name": "amount", "label": "Enter a custom SOL amount"}
],
},
]
},
}
response = make_response(jsonify(data), 200, SOLANA_HEADERS)
if request.method == "OPTIONS":
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = (
"Content-Type,Authorization,Content-Encoding,Accept-Encoding,X-Action-Version,X-Blockchain-Ids"
)
return response
@app.route("/donate/<amount>")
def sol_donate_amount(amount):
data = {
"icon": "https://nathan.woodburn.au/assets/img/profile.png",
"label": f"Donate {amount} SOL to Nathan.Woodburn/",
"title": "Donate to Nathan.Woodburn/",
"description": f"Donate {amount} SOL to Nathan.Woodburn/",
}
return jsonify(data), 200, SOLANA_HEADERS
@app.route("/donate/<amount>", methods=["POST"])
def sol_donate_post(amount):
if not request.json:
return jsonify({"message": "Error: No JSON data provided"}), 400, SOLANA_HEADERS
if "account" not in request.json:
return jsonify({"message": "Error: No account provided"}), 400, SOLANA_HEADERS
sender = request.json["account"]
# Make sure amount is a number
try:
amount = float(amount)
except ValueError:
amount = 1 # Default to 1 SOL if invalid
if amount < 0.0001:
return jsonify({"message": "Error: Amount too small"}), 400, SOLANA_HEADERS
transaction = create_transaction(sender, amount)
return jsonify({"message": "Success", "transaction": transaction}), 200, SOLANA_HEADERS

130
blueprints/spotify.py Normal file
View File

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

9
blueprints/template.py Normal file
View File

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

63
blueprints/wellknown.py Normal file
View File

@@ -0,0 +1,63 @@
from flask import Blueprint, make_response, request, jsonify, send_from_directory, redirect
from tools import error_response
import os
app = Blueprint('well-known', __name__, url_prefix='/.well-known')
@app.route("/<path:path>")
def index(path):
return send_from_directory(".well-known", path)
@app.route("/wallets/<path:path>")
def wallets(path):
if path[0] == "." and 'proof' not in path:
return send_from_directory(
".well-known/wallets", path, mimetype="application/json"
)
elif os.path.isfile(".well-known/wallets/" + path):
address = ""
with open(".well-known/wallets/" + path) as file:
address = file.read()
address = address.strip()
return make_response(address, 200, {"Content-Type": "text/plain"})
if os.path.isfile(".well-known/wallets/" + path.upper()):
return redirect("/.well-known/wallets/" + path.upper(), code=302)
return error_response(request)
@app.route("/nostr.json")
def nostr():
# Get name parameter
name = request.args.get("name")
if name:
return jsonify(
{
"names": {
name: "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82"
}
}
)
return jsonify(
{
"names": {
"nathan": "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82",
"_": "b57b6a06fdf0a4095eba69eee26e2bf6fa72bd1ce6cbe9a6f72a7021c7acaa82",
}
}
)
@app.route("/xrp-ledger.toml")
def xrp():
# Create a response with the xrp-ledger.toml file
with open(".well-known/xrp-ledger.toml") as file:
toml = file.read()
response = make_response(toml, 200, {"Content-Type": "application/toml"})
# Set cors headers
response.headers["Access-Control-Allow-Origin"] = "*"
return response

36
cleanSite.py Normal file
View File

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

132
curl.py Normal file
View File

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

View File

@@ -0,0 +1,27 @@
[View video tutorial](https://cloud.woodburn.au/s/n7Q3k7QyEnwygjX)
Install prerequisites:
```bash
sudo apt install libfuse2 libunbound-dev
```
Download latest release AppImage from SANE version of Fingertip:
[Fingertip Github Repo](https://github.com/randomlogin/fingertip)
Make the AppImage executable:
```bash
chmod +x Fingertip-*.AppImage
```
Run the AppImage:
```bash
./Fingertip-*.AppImage
```
You should see the fingertip notification icon in the system tray, right-click and select Options > Help. This should open a browser window with the Fingertip status page.
Go to the "Manual Setup" page, download the certificate and copy the proxy pac URL.
Open the system settings > Network > Network Proxy. Set the Method to Automatic and paste in the URL.
Next we need to import the certificate authority into your preferred browser.
You can usually just open the settings in the browser and search for "Certificates". Then import the certificate into the "Authorities" tab and make sure you select the option to trust it for identifying websites.

View File

@@ -0,0 +1,28 @@
Setting up a Nameserver for your domains held in BobWallet is needed in order to use your domains for websites or other services.
This guide will walk you through the process of setting up a nameserver using the BobWallet app.
<br>
#### Prerequisites
* [BobWallet](https://bobwallet.io) with at least 1 domain
Once you have your domain in BobWallet, you can set up a nameserver using the HNSAU service. This is a free service that allows you to create a nameserver for your domains.
1. Create an account at [HNSAU's free Nameserver service](https://domains.hns.au)
2. In the Add Site section, enter your domain name. Ensure you don't include any protocols (http:// or https://), subdomains (www.), or trailing slashes (/).
3. You should now see your domain listed in the External Domains section.
4. Click on the manage button next to the domain name to view its details. Keep this page open, as you will need to copy the nameserver and DS info later.
5. In BobWallet, go to Domain Manger and select the domain you want to set up a nameserver for.
6. In the Records section for the domain, remove any existing records with TYPE NS or DS.
7. Click on the Add Record button and select the TYPE NS. Add the NS value from the HNSAU page. Make sure you include the trailing dot (.) at the end of the nameserver. Repeat this for all the Nameservers listed on the HNSAU page.
- ns1.australia.
- ns2.australia.
8. Click on the Add Record button again and select the TYPE DS. Add the DS value from the DNSSEC section in HNSAU. This DS value is unique to each domain and is used to verify the authenticity of the nameserver.
9. Submit the changes and wait for the DNS records to propagate onchain. This will take up to 7 hrs (depending on the next tree update).
10. You can now use the HNSAU nameserver to point your domain to any website or service.
[View demonstration video](https://youtu.be/Ong8A7FDH24)

View File

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

77
data/nathanwoodburn.asc Normal file
View File

@@ -0,0 +1,77 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsFNBGPp+YUBEADrOH051Panj+KMnVCvilPx4L4jSqOH/TwdQIpp3y2JSk5foysY
9/n3AbeUoKi5x+vKp9XNmIZjwSlUcTUo65kx39vlSiMRuRkRkdLGACc0pM8GCKtl
7s016bvX22h5w2jg1d4d5Aq7BUsoRgMlgNWkAhPKzwgR67VYfnLI2sRe2+9P0Aj4
venAZnk0XNHNmL70dHNvG9M9CK11kNGiG2Xqnb4vVTTyLD54i040JCK2xkAOk1PX
JIoIyBu2wAUz3rMczopJWrInDrMiZN56a0bqQQQt8lKf8dD6yNfb1LXJWfuxHlw4
Zjkz2D99zT9J++fRQhwJfye/1sgk069aKXyv1lg0N1cPulkP+7uD5389NKc2wA/b
rw0p2Rr7BnPpz4KlTGaOfU9KmxP1eQ3WpH/FMVLkxun4hNuEeca+/yAw6OCCqB/C
P5SagzDeKTjUdi2yo2KuhHon7U0G+xyCqK7j998H/SIh3T/wjxE8FHCTHH2VuuQ/
9xXMpkXqctCQy83th1YmWkqBDyLioYVs6DpjLb6BytRXajHqXhX94ZLcdxSwjbWh
Evse0PHpQZsDHs1cKCZTmGnH0VUhuPUHykYoNwDdimpLLVpGfkr7s+BgfZCQnSkc
kHIzmZFT3rTFSIsMFa3Kr8HRDqA6ezC5RT7/s5fa4vN0/Hh0eAthMuv4NQARAQAB
zSxOYXRoYW4gV29vZGJ1cm4gPGNvbnRhY3RAbmF0aGFuLndvb2RidXJuLmF1PsLB
dQQTAQgAKQUCY+n5hQkQIDsABHitDvECGwMFCR4TOAAECwcJAwUVCAoCAwQWAAEC
AACTphAAwE4UDHqFy3BkMaQPNOjovhPu0dimj6EFlLqxFoXX7/kWsbZUtHiRuSHY
vm18J6prV9EcpjGbmFSza/PTmA8Jo71/F/rMG9IGRmSUP6aP0GPpuB1WBpbU9sZW
F6hqwfdTaCdAkIMWctFqCb1QVflEWlvIyUsAp90LChWS23m2YxXxc3Je4dwjbvYx
ie2uyMd6lEQuz2aWQkYH2As2RIJsbdrlDK/fc5Z1ebumQPgTDt2WLYPH2sRRzps5
KQkbSAggAFxDs7uuh2pQzJlxTRD9uSk1/RlQoD7YSfxMhqNn7XDCHD/51b2xiB6M
qZSf27iUGAuekoGniKsXNbyh1zG2BSe2pLVwC2Lub/OcnBMPgHQp56iqrchMrc4G
idPwYY2NtuVLFCG8csQcHwnUvxb3PvdgXy3xAvqhjiQXAgGJU8HMJddnhrTBkTtZ
NoE3IfE2mBJa3P1vyIFa3JpsI1+aWX4K8IZAt/weQd58sTIOmES1VGhmKnq/W6q4
Q5vGx5wVqex+YfmTHPcVeM8N3cOwwI/rqH5r5fMBTyc51yPICm26NWTfKPCBIvJM
xHpCffWw+IoRiEC42WPLcmvcobpMdTjj6SUAps1cBwn/rcwtSOrKwCWcX4P9uFE/
TYffDjDV80e1MJurCd9jDdeKnDQzzKYKurIIaBvsDSZfVpK0pYjNJE5hdGhhbiBX
b29kYnVybiA8bmF0aGFuQHdvb2RidXJuLmF1PsLBdQQTAQgAKQUCY+n5hQkQIDsA
BHitDvECGwMFCR4TOAAECwcJAwUVCAoCAwQWAAECAABBLxAAGUlm0dx+vfjR2L+e
/r9wpP8KrGgKYOdeSdm5xUEvbEjrPjYdu+mB+PyinRRs5aDwCG1ehRMoxcDj/Kju
Mn/QV/1uVQQ0BHOfZ3LyiMsnTy10DkmNdbInS0Ek2rbIiDHvbzmE63Uzg8M+9VBF
4Vs30Dc6JFdzWiKuNxiqIWYCL7B7T6pSzLKhohSmkiwX8HgihV2MQ21QDC16SI1o
0oNIyxVICIrbF093fFyFP5kCETq+3y9FTdZD64yZpN/CJDFu5gDfTnX9nNhcfpCD
KbisBvvJC+1hVNvQq6J+3nTWWopfJHs8DDPtXpQzYGjUbaXZsxhvSge0WbB0c693
IeuV71X1JJbI5oIx7YbBH3HkVX8QzhCIQBFzPMsYzb8ozr1feY8G3BpNDIMWR/dg
P4g/dU+nTJKOd+MIsfqBbsmBQ0ofUXX/+dtip6iL5py37g6FdRiM/di0Faf4vVCJ
HwOf1KYBjBP2HniEuY7rldjGwy4IzErYaDxlxdlDjpTW0R6CnoHgLlOmdnbn6kK+
OnHK6Os9q7nRkHNIhxPfVg/q9BWGL0XJ3tRktI4gUKtwYKz3p2wXeu76vz2A1vFp
oNbtO18lTa20Lbw3QOlrnSfXOFB/KU6mlQqDd1HPP3/F0Ml9VFKEJ2o8JfidnaPQ
UvhXXXsGtBzwcUle8dBiW3T/zdDNK05hdGhhbiBXb29kYnVybiA8Z2l0aHViQG5h
dGhhbi53b29kYnVybi5hdT7CwXgEEwEIACwFAmPp+YUJECA7AAR4rQ7xAhsDBQke
EzgAAhkBBAsHCQMFFQgKAgMEFgABAgAAXmEQAKkC1otp0Bhb+gjloEGvbXf9P+ol
8oguTqxqVd723nquSALh2VVYFww1nU5RrMO99ds1RiS1ymYXGWVbbaV6gP2vUff6
D7Y4bFxNQUtsTcRD1ZAcLwivF6vm5bgLNi/QEtzfW6/Hgfv08WoX6G9UUfrJfm5O
29H2JkE5jI1DSB/4r0Awd+HZjLcpn3WH6HeXcx9ui6DXCH8FzEGsxCuRkw5m7nGj
1BQ2MBzBli8519ak73Dq9HGSN+zQR8hRsJEuJLy4oExz8d4Zt5anDxJT5C0Ynr0x
hHv7of3AtG1eP8gz934iRvauKdTlzzvn/h0NTPpOe55rRUTMTw8WyM55wcfrnN5K
Y8MLgnIkxflRLv6PlKJtMPlKPat9My9pAaUifot9qFMBxRD2pxFLZzxFLS/qGWOP
OZldm58Dz+NqGtz3ye+PPwDd0/a1lGD6WWaUZsnzXjZE6YbRsWUegF31Lbb2hzSk
8iipXw6hhfDvrCXToYeGbh4OMCZVHKZwKK8fkEnnJgPbZsY0SVcD+aALa8rVLp11
hoNFQyGPsHgILL0tAXGpEJ+EI5C6iS+/tQQFrGxgNvp90KOdditvNszeoDVrrbMo
kiXACV27RS1/eR935SPBlKmUUpaMWwUA3wl6OJ7k09nMwVe0AyWC1yh4M/VOe5JB
DgBeUzfvTzMEagkCzsFNBGPp+YUBEADWPlyLWeuNwWvR5x+weolaUwisFV0apfH8
oFlrJfLvwkpwqtYnySW916tNrW46blMjI7ZJaMNGWkF0g2uJBhpX8UYV+HPEBoNv
S1vXECpvb/126xfGkpAmMELj5zypaqdyLP0DppHp9NGGznYysZ9CTM0OdolLW5XT
wilRk7v+LSIymL83EovbV+4vc81AO2Aq1aG3lQmTukZOfp23Y0Nk2to5H9rCCo7s
+PQRTPefFAYDwj0CXIMNckOs1eAMHX+rDU/f4ojUkC5uGVci3fCPdZbHCLOwW0NZ
DcwjZW5H7XELUfmKg0lfe2qYQo0CFJ+gco6B0cJ/jlQB+E+X76wcaICXKfYdoT8o
mA7za+JVfBWKqVYbwk6MnCHhp/Pj9KUokdonmNWA22/v+r8Ox/b7clp3CAarHyI8
mwfhfDWHutRVqxSb+Lw4jN5Qy0q1YXmN9mUYlrNXV5nWwz+k25sijpxXoNJGdjOj
I4VFlWzalGxmOAFrmM4YkoHb6i5RZIRVTVVSxDhtin5oW+XLgC1d58r9mday/i1j
jyVT1GBWpIf/DtyTUoOhqZXcFaGnF+/ZMCWnpRWbdI1dBm2g+NXBdhsz8e7fJC2P
4CvU+eqityahq/X0YrI4Xe35okfuVFSDfdSjTH/rBwdYHFp/8REkn1MnXfPF2nLY
uM/zXxVGJQARAQABwsF1BBgBCAApBQJj6fmFCRAgOwAEeK0O8QIbDAUJHhM4AAQL
BwkDBRUICgIDBBYAAQIAADv8EAA44qa8juqt17lhYo32dveMlXdyshLNHYlFuZlg
fy729x1j2mZgSrkCv7QwK9Mk9PJGb6YX9pyilr3S+AcYoZnSL0cVV+LAeJ5InjMo
22g094/qcVZmiH3CNz1OuknwnkDkwHareUmHbM9a3DGBJQ7SN55PRFIZccU/DrXG
NcEkSmfl/RJMNizolgRDz8S1XS0MZmG6/xrX7kxK2SfuXlxaDgMWoCAaxoil2MW2
BXxhwZ8GQayuZKJGdTc/iDzk6C7dkQCoBfdxDWGeY1yACfcbAiRA/u5gdpFg6+Wm
IUWwchpPHZmUozcuiPWQX5f3w7pzMMHYzov8otu5vsuPbnAuc1OcwSFXTb4FP98G
B7ORBWU/xvmMz5vqfcywY3bdr8938GJXs7MxqcXJJoMivUYzUGHSw4zf5tOxnltq
AFZjP2muCOBwDKDm8c3/Q3lqZkijIn/iiolSNhNHQZlbuP/57+1XMDBOrHYwhUFB
zcpFkUrFho23Gwia2Q3lkn129qbFW7J5dMVizAwvt3DnsTZYDl3KgIWQwgId4BMi
Rk2DJK5d65l1qg7f6w2pNaVG3i5Om+t7Z22UNuCJT/HG/cP6F9es0rAaNFXXxdRS
/G749MtEVVLiCbHNE4ZfWfXgAuiw9KIQaD/tCostZIEbJgwOePMXxXQWCR6V4yfA
i0GVXA==
=W9Zx
-----END PGP PUBLIC KEY BLOCK-----

BIN
data/resume.pdf Normal file

Binary file not shown.

View File

@@ -3,13 +3,15 @@
"url": "https://nathan3dprinting.au", "url": "https://nathan3dprinting.au",
"img": "/assets/img/external/nathan3dprinting.webp", "img": "/assets/img/external/nathan3dprinting.webp",
"name": "Nathan 3D Printing", "name": "Nathan 3D Printing",
"description": "Offering 3D Printing and CAD modelling services to the Canberra region" "description": "Offering 3D Printing and CAD modelling services to the Canberra region",
"enabled": false
}, },
{ {
"url": "https://domains.hns.au", "url": "https://domains.hns.au",
"img": "/assets/img/external/HNSAU.webp", "img": "/assets/img/external/HNSAU.webp",
"name": "HNSAU Registry", "name": "HNSAU Registry",
"description": "An easy to use DNS provider and domain reselling platform" "description": "An easy to use DNS provider and domain reselling platform",
"enabled": false
}, },
{ {
"url": "https://hns.au", "url": "https://hns.au",
@@ -21,7 +23,8 @@
"url": "https://hnshosting.au", "url": "https://hnshosting.au",
"img": "/favicon.png", "img": "/favicon.png",
"name": "HNS Hosting", "name": "HNS Hosting",
"description": "Simple Wordpress hosting for Handshake domains with builtin SSL using DANE" "description": "Simple Wordpress hosting for Handshake domains with builtin SSL using DANE",
"enabled": false
}, },
{ {
"url": "https://firewallet.au", "url": "https://firewallet.au",
@@ -33,7 +36,8 @@
"url": "https://shakecities.com", "url": "https://shakecities.com",
"img": "/assets/img/external/HNSW.png", "img": "/assets/img/external/HNSW.png",
"name": "ShakeCities", "name": "ShakeCities",
"description": "A single page website creator where each user's page on their free HNS domain" "description": "A single page website creator where each user's page on their free HNS domain",
"enabled": false
}, },
{ {
"url": "https://git.woodburn.au", "url": "https://git.woodburn.au",
@@ -45,6 +49,37 @@
"url": "https://linkr", "url": "https://linkr",
"img": "/favicon.png", "img": "/favicon.png",
"name": "LINKR/", "name": "LINKR/",
"description": "A free link shortener with a Handshake TLD and using DNS for authentication" "description": "A free link shortener with a Handshake TLD and using DNS for authentication",
"enabled": false
},
{
"url": "https://faucet.woodburn.au",
"img": "/favicon.png",
"name": "HNS Domain Faucet",
"description": "A service providing free Handshake TLDs to allow for quick testing for new users"
},
{
"url":"https://hnsdoh.com",
"img": "/assets/img/external/HNS.png",
"name": "HNS DoH",
"description": "A DNS over HTTPS resolver for Handshake domains"
},
{
"url": "https://ipfs.hnsproxy.au",
"img": "https://ipfs.hnsproxy.au/fireportal.png",
"name": "FirePortal",
"description": "A Handshake domain IPFS gateway that allows you to access IPFS content using Handshake domains"
},
{
"url": "https://hsd.hns.au/",
"img": "/favicon.png",
"name": "Fire HSD",
"description": "A free public API for Handshake (HSD)"
},
{
"url": "https://time.c.woodburn.au/",
"img": "/favicon.png",
"name": "Timezone Converter",
"description": "A simple site and API for converting timezones"
} }
] ]

171
data/tools.json Normal file
View File

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

115
mail.py Normal file
View File

@@ -0,0 +1,115 @@
import smtplib
import re
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.utils import formataddr
from flask import jsonify
import os
# This is used to send emails via API
# The process should be something like this
# curl --request POST \
# --url https://nathan.c.woodburn.au/api/email \
# --header 'Content-Type: application/json' \
# --data '{
# "key":"api-key",
# "to": "recipient@nathan.woodburn.au",
# "from": "sender@nathan.woodburn.au",
# "sender":"Nathan.Woodburn/",
# "subject":"Test email from api",
# "body":"G'\''day\nThis is a test email from my website api\n\nRegards,\nNathan.Woodburn/"
# }'
def validateSender(email):
domains = os.getenv("EMAIL_DOMAINS")
if not domains:
return False
domains = domains.split(",")
for domain in domains:
if re.match(r".+@" + domain, email):
return True
return False
def sendEmail(data):
fromEmail = "noreply@woodburn.au"
if "from" in data:
fromEmail = data["from"]
if not validateSender(fromEmail):
return jsonify({
"status": 400,
"message": "Bad request 'from' email invalid"
})
if "to" not in data:
return jsonify({
"status": 400,
"message": "Bad request 'to' json data missing"
})
to = data["to"]
if "subject" not in data:
return jsonify({
"status": 400,
"message": "Bad request 'subject' json data missing"
})
subject = data["subject"]
if "body" not in data:
return jsonify({
"status": 400,
"message": "Bad request 'body' json data missing"
})
body = data["body"]
if not re.match(r"[^@]+@[^@]+\.[^@]+", to):
raise ValueError("Invalid recipient email address.")
if not subject:
raise ValueError("Subject cannot be empty.")
if not body:
raise ValueError("Body cannot be empty.")
fromName = "Nathan Woodburn"
if 'sender' in data:
fromName = data['sender']
# Create the email message
msg = MIMEMultipart()
msg['From'] = formataddr((fromName, fromEmail))
msg['To'] = to
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain'))
# Sending the email
try:
host = os.getenv("EMAIL_SMTP")
user = os.getenv("EMAIL_USER")
password = os.getenv("EMAIL_PASS")
if host is None or user is None or password is None:
return jsonify({
"status": 500,
"error": "Email server not configured"
})
with smtplib.SMTP_SSL(host, 465) as server:
server.login(user, password)
server.sendmail(fromEmail, to, msg.as_string())
print("Email sent successfully.")
return jsonify({
"status": 200,
"message": "Send email successfully"
})
except Exception as e:
return jsonify({
"status": 500,
"error": "Sending email failed",
"exception":e
})

10
main.py
View File

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

26
pwa/manifest.json Normal file
View File

@@ -0,0 +1,26 @@
{
"short_name": "Nathan.Woodburn/",
"name": "Nathan.Woodburn/",
"icons": [
{
"src": "https://nathan.woodburn.au/assets/img/favicon/android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "https://nathan.woodburn.au/assets/img/favicon/android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "https://nathan.woodburn.au",
"background_color": "#000000",
"theme_color": "#000000",
"display": "fullscreen",
"orientation": "portrait",
"id": "nathanwoodburn",
"description": "Nathan.Woodburn/",
"scope": "https://nathan.woodburn.au",
"dir": "ltr",
"lang": "en"
}

37
pwa/sw.js Normal file
View File

@@ -0,0 +1,37 @@
// This is the service worker with the combined offline experience (Offline page + Offline copy of pages)
const CACHE = "pwabuilder-offline-page";
importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js');
const PRECACHE_ASSETS = [
'/',
'/404',
'/assets/bootstrap/css/bootstrap.min.css',
'https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&display=swap',
'https://fonts.googleapis.com/css?family=Cabin:700&display=swap',
'https://fonts.googleapis.com/css?family=Anonymous+Pro&display=swap',
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700',
'/assets/css/styles.min.css',
'/assets/css/404.min.css',
'/assets/css/profile.min.css',
'/assets/bootstrap/js/bootstrap.min.js',
'/assets/js/script.min.js',
'/assets/js/404.min.js',
'/assets/img/favicon/favicon-16x16.png',
'/assets/img/favicon/android-chrome-192x192.png'
]
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
self.addEventListener('install', async (event) => {
event.waitUntil(
caches.open(CACHE)
.then((cache) => cache.addAll(PRECACHE_ASSETS))
);
});

25
pyproject.toml Normal file
View File

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

View File

@@ -1,8 +0,0 @@
flask
Flask-Cors
python-dotenv
gunicorn
requests
cloudflare
qrcode
ansi2html

935
server.py

File diff suppressed because it is too large Load Diff

53
templates/403.html Normal file
View File

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

View File

@@ -5,19 +5,19 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Nathan.Woodburn/</title> <title>Nathan.Woodburn/</title>
<meta name="theme-color" content="#97009a"> <meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/404"> <link rel="canonical" href="https://nathan.woodburn.au/404">
<meta property="og:url" content="https://nathan.woodburn.au/404"> <meta property="og:url" content="https://nathan.woodburn.au/404">
<meta http-equiv="onion-location" content="http://wdbrncwefot4hd7bdrz5rzb74mefay7zvrjn2vmkpdm44l7fwnih5ryd.onion"> <meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects."> <meta name="twitter:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta name="description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:title" content="Nathan.Woodburn/"> <meta property="og:title" content="Nathan.Woodburn/">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg"> <meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta name="twitter:title" content="Nathan.Woodburn/"> <meta name="twitter:title" content="Nathan.Woodburn/">
<meta property="og:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects."> <meta property="og:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta name="description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png"> <link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
@@ -29,10 +29,12 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/css/styles.min.css"> <link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/404.min.css"> <link rel="stylesheet" href="/assets/css/404.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css"> <link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" /> <link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script> <script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head> </head>
@@ -44,6 +46,7 @@
</div> </div>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script> <script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script> <script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/404.min.js"></script> <script src="/assets/js/404.min.js"></script>
</body> </body>

View File

@@ -5,19 +5,19 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Nathan.Woodburn/</title> <title>Nathan.Woodburn/</title>
<meta name="theme-color" content="#97009a"> <meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/about"> <link rel="canonical" href="https://nathan.woodburn.au/about">
<meta property="og:url" content="https://nathan.woodburn.au/about"> <meta property="og:url" content="https://nathan.woodburn.au/about">
<meta http-equiv="onion-location" content="http://wdbrncwefot4hd7bdrz5rzb74mefay7zvrjn2vmkpdm44l7fwnih5ryd.onion"> <meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects."> <meta name="twitter:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta name="description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:title" content="Nathan.Woodburn/"> <meta property="og:title" content="Nathan.Woodburn/">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg"> <meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta name="twitter:title" content="Nathan.Woodburn/"> <meta name="twitter:title" content="Nathan.Woodburn/">
<meta property="og:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects."> <meta property="og:description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta name="description" content="G'day, this is my personal website. You can find out about me or check out some of my projects.">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png"> <link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
@@ -29,12 +29,14 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/fontawesome-all.min.css"> <link rel="stylesheet" href="/assets/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/assets/fonts/ionicons.min.css"> <link rel="stylesheet" href="/assets/fonts/ionicons.min.css">
<link rel="stylesheet" href="/assets/css/styles.min.css"> <link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/fixes.min.css"> <link rel="stylesheet" href="/assets/css/fixes.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css"> <link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" /> <link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script> <script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head> </head>
@@ -42,42 +44,74 @@
<body class="about-body" style="text-align: center;color: rgb(255,255,255);background: transparent;">{{handshake_scripts | safe}} <body class="about-body" style="text-align: center;color: rgb(255,255,255);background: transparent;">{{handshake_scripts | safe}}
<div class="profile-container" style="margin-bottom: 2em;margin-top: 5em;"><img class="profile background" src="/assets/img/profile.jpg" style="border-radius: 50%;"><img class="profile foreground" src="/assets/img/pfront.webp"></div> <div class="profile-container" style="margin-bottom: 2em;margin-top: 5em;"><img class="profile background" src="/assets/img/profile.jpg" style="border-radius: 50%;"><img class="profile foreground" src="/assets/img/pfront.webp"></div>
<h1 class="nathanwoodburn" style="margin-bottom: 0.5em;">Nathan.Woodburn/</h1> <h1 class="nathanwoodburn" style="margin-bottom: 0.5em;">Nathan.Woodburn/</h1>
<div class="container"> <section class="text-center content-section" id="contact" style="padding-top: 0px;padding-bottom: 3em;">
<div class="row"> <div class="container">
<div class="col-lg-8 mx-auto"> <div class="row">
<div class="social-div"> <div class="col-lg-8 d-none d-print-block d-sm-block d-md-block d-lg-block d-xl-block d-xxl-block mx-auto">
<ul class="list-unstyled social-list"> <div class="social-div">
<li class="social-link"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon"> <ul class="list-unstyled social-list">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path> <li class="social-link"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon">
</svg></a></li> <path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
<li class="social-link"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon"> </svg></a></li>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path> <li class="social-link"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon">
</svg></a></li> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
<li class="social-link"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon"></i></a></li> </svg></a></li>
<li class="social-link discord"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon"> <li class="social-link"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon"></i></a></li>
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path> <li class="social-link discord"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon">
</svg></a></li> <path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
</ul> </svg></a></li>
</ul>
</div>
<div class="social-div">
<ul class="list-unstyled social-list">
<li class="social-link mastodon"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
</svg></a></li>
<li class="social-link youtube"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
</svg></a></li>
<li class="social-link signal"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon">
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
</svg></a></li>
</ul>
</div>
</div> </div>
<div class="social-div"> <div class="col-lg-8 d-block d-print-none d-sm-none d-md-none d-lg-none d-xl-none d-xxl-none mx-auto">
<ul class="list-unstyled social-list"> <div class="social-div">
<li class="social-link mastodon"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon"> <ul class="list-unstyled social-list-sml">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path> <li class="social-link-sml"><a href="https://twitter.com/woodburn_nathan" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x icon-sml">
</svg></a></li> <path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
<li class="social-link youtube"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon"> </svg></a></li>
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path> <li class="social-link-sml"><a href="https://github.com/Nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-github icon-sml">
</svg></a></li> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"></path>
<li class="social-link signal"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon"> </svg></a></li>
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path> <li class="social-link-sml"><a href="mailto:about@nathan.woodburn.au" target="_blank"><i class="icon ion-email icon-sml"></i></a></li>
</svg></a></li> <li class="discord social-link-sml"><a href="https://l.woodburn.au/discord" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-discord icon-sml">
</ul> <path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"></path>
</svg></a></li>
</ul>
</div>
<div class="social-div">
<ul class="list-unstyled social-list-sml">
<li class="mastodon social-link-sml"><a href="https://mastodon.woodburn.au/@nathanwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-mastodon icon-sml">
<path d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"></path>
</svg></a></li>
<li class="youtube social-link-sml"><a href="https://www.youtube.com/@nathanjwoodburn" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-youtube icon-sml">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"></path>
</svg></a></li>
<li class="signal social-link-sml"><a href="/signalQR" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-signal icon-sml">
<path d="m6.08.234.179.727a7.264 7.264 0 0 0-2.01.832l-.383-.643A7.9 7.9 0 0 1 6.079.234zm3.84 0L9.742.96a7.265 7.265 0 0 1 2.01.832l.388-.643A7.957 7.957 0 0 0 9.92.234zm-8.77 3.63a7.944 7.944 0 0 0-.916 2.215l.727.18a7.264 7.264 0 0 1 .832-2.01l-.643-.386zM.75 8a7.3 7.3 0 0 1 .081-1.086L.091 6.8a8 8 0 0 0 0 2.398l.74-.112A7.262 7.262 0 0 1 .75 8m11.384 6.848-.384-.64a7.23 7.23 0 0 1-2.007.831l.18.728a7.965 7.965 0 0 0 2.211-.919zM15.251 8c0 .364-.028.727-.082 1.086l.74.112a7.966 7.966 0 0 0 0-2.398l-.74.114c.054.36.082.722.082 1.086m.516 1.918-.728-.18a7.252 7.252 0 0 1-.832 2.012l.643.387a7.933 7.933 0 0 0 .917-2.219zm-6.68 5.25c-.72.11-1.453.11-2.173 0l-.112.742a7.99 7.99 0 0 0 2.396 0l-.112-.741zm4.75-2.868a7.229 7.229 0 0 1-1.537 1.534l.446.605a8.07 8.07 0 0 0 1.695-1.689l-.604-.45zM12.3 2.163c.587.432 1.105.95 1.537 1.537l.604-.45a8.06 8.06 0 0 0-1.69-1.691l-.45.604zM2.163 3.7A7.242 7.242 0 0 1 3.7 2.163l-.45-.604a8.06 8.06 0 0 0-1.691 1.69l.604.45zm12.688.163-.644.387c.377.623.658 1.3.832 2.007l.728-.18a7.931 7.931 0 0 0-.916-2.214M6.913.831a7.254 7.254 0 0 1 2.172 0l.112-.74a7.985 7.985 0 0 0-2.396 0l.112.74zM2.547 14.64 1 15l.36-1.549-.729-.17-.361 1.548a.75.75 0 0 0 .9.902l1.548-.357-.17-.734zM.786 12.612l.732.168.25-1.073A7.187 7.187 0 0 1 .96 9.74l-.727.18a8 8 0 0 0 .736 1.902l-.184.79zm3.5 1.623-1.073.25.17.731.79-.184c.6.327 1.239.574 1.902.737l.18-.728a7.197 7.197 0 0 1-1.962-.811l-.007.005zM8 1.5a6.502 6.502 0 0 0-6.498 6.502 6.516 6.516 0 0 0 .998 3.455l-.625 2.668L4.54 13.5a6.502 6.502 0 0 0 6.93-11A6.516 6.516 0 0 0 8 1.5"></path>
</svg></a></li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </section>
<p style="margin-top: 1em;">Hi, I am Nathan Woodburn and I live in Canberra<br>I am currently studying at the Australian National University<br>I enjoy 3D printing and CAD<br>I code stuff with C#, Linux Bash and tons of other languages<br>I'm a co-founder of <a href="https://hns.au" target="_blank">Handshake Australia</a><br>I currently work for <a href="https://learn.namebase.io" target="_blank">Namebase</a><br><br></p><i class="fas fa-arrow-down" style="font-size: 50px;" onclick="slideout()"></i> <p style="margin-top: 1em;">Hi, I am Nathan Woodburn and I live in Canberra<br>I am currently studying at the Australian National University<br>I enjoy managing linux servers for my various projects<br>I code stuff with C#, Linux Bash and tons of other languages<br>I'm a co-founder of <a href="https://hns.au" target="_blank">Handshake Australia</a><br><br></p><i class="fas fa-arrow-down" style="font-size: 50px;" onclick="slideout()"></i>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script> <script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script> <script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/about.min.js"></script> <script src="/assets/js/about.min.js"></script>
<script src="/assets/js/hacker.min.js"></script> <script src="/assets/js/hacker.min.js"></script>
</body> </body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
.social-icons{color:#313437;background-color:#fff;padding:70px 0}@media (max-width:767px){.social-icons{padding:50px 0}}@media (max-width:500px){img.profile{width:200px;margin-left:-100px}.profile-container{height:200px;margin-top:2em!important}}.social-div{display:flex;justify-content:center;align-items:center}.social-list-sml{display:flex;list-style:none;gap:1rem}.social-list{display:flex;list-style:none;gap:2.5rem}.social-icons i{color:#757980;margin:0 10px;width:60px;height:60px;border:1px solid #c8ced7;text-align:center;border-radius:50%;line-height:60px;display:inline-block}.social-link-sml a{text-decoration:none;width:3.5rem;height:3.5rem;background-color:#f0f9fe;border-radius:50%;display:flex;justify-content:center;align-items:center;position:relative;z-index:1;border:3px solid #f0f9fe;overflow:hidden}.social-link a{text-decoration:none;width:4.8rem;height:4.8rem;background-color:#f0f9fe;border-radius:50%;display:flex;justify-content:center;align-items:center;position:relative;z-index:1;border:3px solid #f0f9fe;overflow:hidden}.social-link a::before,.social-link-sml a::before{content:"";position:absolute;width:100%;height:100%;background:var(--bg-color);z-index:0;scale:1 0;transform-origin:bottom;transition:scale .5s}.social-link-sml:hover a::before,.social-link:hover a::before{scale:1 1}.icon-sml{font-size:1.5rem;color:#011827;transition:.5s;z-index:2}.icon{font-size:2rem;color:#011827;transition:.5s;z-index:2}.social-link a:hover .icon{color:#fff;transform:rotateY(360deg)}.social-link,.social-link-sml{--bg-color:#000}.social-link-sml.discord,.social-link.discord{--bg-color:#5865f2}.social-link-sml.mastodon,.social-link.mastodon{--bg-color:#6364ff}.social-link-sml.youtube,.social-link.youtube{--bg-color:#ff0000}.social-link-sml.signal,.social-link.signal{--bg-color:#365eb6}

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

@@ -0,0 +1 @@
p{margin:auto!important}pre{line-height:125%}li{list-style:inside}.social-link{list-style:none!important}span.linenos,td.linenos .normal{color:inherit;background-color:transparent;padding-left:5px;padding-right:5px}span.linenos.special,td.linenos .special{color:#000;background-color:#ffffc0;padding-left:5px;padding-right:5px}.codehilite .hll{background-color:#ffc}.codehilite{background:#f8f8f8;color:#333;width:fit-content;margin:auto;padding:0 5px;border-radius:5px}.codehilite .c,.codehilite .c1,.codehilite .ch,.codehilite .cm,.codehilite .cpf,.codehilite .cs{color:#3d7b7b;font-style:italic}.codehilite .err{border:1px solid red}.codehilite .k,.codehilite .kc,.codehilite .kd,.codehilite .kn,.codehilite .kr,.codehilite .nt{color:green;font-weight:700}.codehilite .il,.codehilite .m,.codehilite .mb,.codehilite .mf,.codehilite .mh,.codehilite .mi,.codehilite .mo,.codehilite .o{color:#666}.codehilite .cp{color:#9c6500}.codehilite .gd{color:#a00000}.codehilite .ge{font-style:italic}.codehilite .ges{font-weight:700;font-style:italic}.codehilite .gr{color:#e40000}.codehilite .gh,.codehilite .gp{color:navy;font-weight:700}.codehilite .gi{color:#008400}.codehilite .go{color:#717171}.codehilite .gs{font-weight:700}.codehilite .gu{color:purple;font-weight:700}.codehilite .gt{color:#04d}.codehilite .bp,.codehilite .kp,.codehilite .nb,.codehilite .sx{color:green}.codehilite .kt{color:#b00040}.codehilite .dl,.codehilite .s,.codehilite .s1,.codehilite .s2,.codehilite .sa,.codehilite .sb,.codehilite .sc,.codehilite .sh{color:#ba2121}.codehilite .na{color:#687822}.codehilite .nc,.codehilite .nn{color:#00f;font-weight:700}.codehilite .no{color:#800}.codehilite .nd{color:#a2f}.codehilite .ni{color:#717171;font-weight:700}.codehilite .ne{color:#cb3f38;font-weight:700}.codehilite .fm,.codehilite .nf{color:#00f}.codehilite .nl{color:#767600}.codehilite .nv,.codehilite .ss,.codehilite .vc,.codehilite .vg,.codehilite .vi,.codehilite .vm{color:#19177c}.codehilite .ow{color:#a2f;font-weight:700}.codehilite .w{color:#bbb}.codehilite .sd{color:#ba2121;font-style:italic}.codehilite .se{color:#aa5d1f;font-weight:700}.codehilite .si{color:#a45a77;font-weight:700}.codehilite .sr{color:#a45a77}

View File

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

View File

@@ -1 +1 @@
#sites{display:flex;flex-direction:column;justify-content:center;align-items:center;width:100%;padding:60px;font-family:Quicksand,sans-serif}.site-container{background:rgba(133,133,133,.2);box-shadow:0 4px 30px rgba(0,0,0,.1);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.2);border-radius:25px;padding:30px 0;width:min(1200px,100%)}.site-container>h1{font-size:2rem;font-weight:600;text-align:center;color:#dda3b6;margin:20px 0 40px}.swiper{width:80%;height:100%;margin-bottom:30px}.swiper-scrollbar{--swiper-scrollbar-bottom:0px;--swiper-scrollbar-drag-bg-color:#dda3b6;--swiper-scrollbar-size:5px}.site{position:relative;max-width:400px;padding:1rem;font-family:inherit;font-size:1rem;font-weight:500;color:var(--clr-text);background-color:transparent;border-radius:10px;isolation:isolate;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.site::before{content:"";position:absolute;top:0;left:0;right:0;bottom:15px;background:rgba(236,149,200,.2);box-shadow:0 4px 30px rgba(0,0,0,.1);border-radius:10px;z-index:-1}.site-img{width:100%;max-width:400px;object-fit:cover;overflow:hidden;aspect-ratio:1;border-radius:6px}.site-body{align-items:center;gap:8px;padding:15px 0;cursor:default}.site-name{font-size:.9rem;font-weight:600;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.site-author{width:fit-content;font-size:.8rem;font-weight:600;opacity:.6;color:var(--clr-text)}.site-avatar{width:40px;aspect-ratio:1/1;object-fit:cover;border-radius:5px;cursor:pointer}.site-actions{position:relative}.site-actions-content{position:absolute;bottom:130%;right:0;padding:8px;border-radius:8px;background:rgba(172,172,172,.2);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:2px 2px 10px 2px hsl(0,0%,0%,.25);transition:opacity .25s,scale .25s;transform-origin:bottom right}.site-actions-content[data-visible=false]{pointer-events:none;opacity:0;scale:0}.site-actions-content[data-visible=true]{pointer-events:unset;scale:1;opacity:1}.site-actions-content li{padding:.5rem .65rem;border-radius:.25rem;list-style:none}.site-actions-content li:is(:hover,:focus-within){background-color:rgba(248,132,169,.7)}.site-actions-link{width:max-content;display:grid;grid-template-columns:1rem 1fr;align-items:center;gap:.6rem;color:inherit;text-decoration:none;cursor:pointer}.site-like{text-decoration:none;color:var(--clr-text);margin-right:5px;font-size:1.1rem;opacity:.65;border-radius:50%;overflow:hidden;transition:.35s}.site-actions-controller{border:0;background:0 0;color:var(--clr-text);cursor:pointer;opacity:.65}.site-actions-controller:hover,.site-like:hover{opacity:1}.site-like:focus{outline:0}.site-like.active{color:red;opacity:1;transform:scale(1.2)}@media (max-width:1200px){.swiper{width:80%}}@media (max-width:900px){#sites{padding:60px 80px}.swiper{width:50%}}@media (max-width:765px){.swiper{width:70%}}@media (max-width:550px){#sites{padding:40px}.swiper{width:80%}}img.no-drag{pointer-events:none}img.fog{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%}#downtime{z-index:2;position:fixed;right:0;bottom:0;width:20%;transition:opacity .5s;opacity:0;cursor:pointer}blockquote.speech{position:absolute;display:inline-block;right:14vw;bottom:23vh;width:17vw;background:url(/assets/img/speech-bubble.svg) center;color:#000;padding-top:3%;padding-bottom:20%;background-repeat:no-repeat!important;margin:0 auto;text-align:center;box-sizing:content-box;line-height:1;font-family:SequentialistBB,cursive;font-size:1.2vw} #sites{display:flex;flex-direction:column;justify-content:center;align-items:center;width:100%;padding:60px;font-family:Quicksand,sans-serif}.site-container{background:rgba(133,133,133,.2);box-shadow:0 4px 30px rgba(0,0,0,.1);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.2);border-radius:25px;padding:30px 0;width:min(1200px,100%)}.site-container>h1{font-size:2rem;font-weight:600;text-align:center;color:#dda3b6;margin:20px 0 40px}.swiper{width:80%;height:100%;margin-bottom:30px}.swiper-scrollbar{--swiper-scrollbar-bottom:0px;--swiper-scrollbar-drag-bg-color:#dda3b6;--swiper-scrollbar-size:5px}.site{position:relative;max-width:400px;padding:1rem;font-family:inherit;font-size:1rem;font-weight:500;color:var(--clr-text);background-color:transparent;border-radius:10px;isolation:isolate;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.site::before{content:"";position:absolute;top:0;left:0;right:0;bottom:15px;background:rgba(236,149,200,.2);box-shadow:0 4px 30px rgba(0,0,0,.1);border-radius:10px;z-index:-1}.site-img{width:100%;max-width:400px;object-fit:cover;overflow:hidden;aspect-ratio:1;border-radius:6px}.site-body{align-items:center;gap:8px;padding:15px 0;cursor:default}.site-name{font-size:.9rem;font-weight:600;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.site-author{width:fit-content;font-size:.8rem;font-weight:600;opacity:.6;color:var(--clr-text)}.site-avatar{width:40px;aspect-ratio:1/1;object-fit:cover;border-radius:5px;cursor:pointer}.site-actions{position:relative}.site-actions-content{position:absolute;bottom:130%;right:0;padding:8px;border-radius:8px;background:rgba(172,172,172,.2);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);box-shadow:2px 2px 10px 2px hsl(0,0%,0%,.25);transition:opacity .25s,scale .25s;transform-origin:bottom right}.site-actions-content[data-visible=false]{pointer-events:none;opacity:0;scale:0}.site-actions-content[data-visible=true]{pointer-events:unset;scale:1;opacity:1}.site-actions-content li{padding:.5rem .65rem;border-radius:.25rem;list-style:none}.site-actions-content li:is(:hover,:focus-within){background-color:rgba(248,132,169,.7)}.site-actions-link{width:max-content;display:grid;grid-template-columns:1rem 1fr;align-items:center;gap:.6rem;color:inherit;text-decoration:none;cursor:pointer}.site-like{text-decoration:none;color:var(--clr-text);margin-right:5px;font-size:1.1rem;opacity:.65;border-radius:50%;overflow:hidden;transition:.35s}.site-actions-controller{border:0;background:0 0;color:var(--clr-text);cursor:pointer;opacity:.65}.site-actions-controller:hover,.site-like:hover{opacity:1}.site-like:focus{outline:0}.site-like.active{color:red;opacity:1;transform:scale(1.2)}@media (max-width:1200px){.swiper{width:80%}}@media (max-width:900px){#sites{padding:60px 80px}.swiper{width:50%}}@media (max-width:765px){.swiper{width:70%}}@media (max-width:550px){#sites{padding:40px}.swiper{width:80%}}img.no-drag{pointer-events:none}img.fog{pointer-events:none;position:absolute;left:0;top:0;width:100%;height:100%}#downtime{z-index:2;position:fixed;right:0;bottom:0;width:20%;transition:opacity .5s;opacity:0;cursor:pointer}blockquote.speech{position:absolute;display:inline-block;right:14vw;bottom:23vh;width:17vw;background:url(/assets/img/speech-bubble.svg) center;color:#000;padding-top:3%;padding-bottom:20%;background-repeat:no-repeat!important;margin:0 auto;text-align:center;box-sizing:content-box;line-height:1;font-family:SequentialistBB,cursive;font-size:1.2vw}.clock{bottom:0;position:fixed}html{scroll-margin-top:4rem}

View File

@@ -1 +1 @@
.profile-container{height:300px}img.profile{width:300px;position:absolute;left:50%;margin-left:-150px;aspect-ratio:1;padding-top:calc(var(--s)/5);transform:scale(1);transition:.5s}img.foreground{border-radius:50%;pointer-events:none}img.background:hover{filter:blur(5px)} .profile-container{height:300px}img.profile{width:300px;position:absolute;left:50%;margin-left:-150px;aspect-ratio:1;padding-top:calc(var(--s)/5);transform:scale(1);transition:filter .3s ease-in-out}img.foreground{border-radius:50%;pointer-events:none}img.background:hover{filter:blur(5px)}.address{max-width:100%}

View File

@@ -1 +1 @@
.profile-container{height:170px;width:170px;z-index:2;left:10%}.title{position:absolute;margin-left:calc(100px);width:calc(100% - 100px);padding:1em;margin-top:-225px;z-index:0}.title>*{width:100%;margin-bottom:0}img.profile{left:10px;width:150px;position:absolute;aspect-ratio:1;transform:scale(1);transition:.5s;z-index:2}img.background2{left:0;width:170px!important;margin-top:-10px;pointer-events:none;z-index:1}img.foreground{border-radius:50%;pointer-events:none;z-index:3}img.background:hover,img.backgroundsml:hover{filter:blur(5px)}.spacer{height:100px}img.profilesml{width:150px;position:absolute;left:50%;margin-left:-75px;aspect-ratio:1;padding-top:calc(var(--s)/5);transform:scale(1);transition:.5s}img.foregroundsml{border-radius:50%;pointer-events:none}img.background2sml{width:170px!important;left:calc(50% - 10px);margin-top:-10px;pointer-events:none;z-index:0}@media print{div{color:#000!important}.noprintbreak{page-break-inside:avoid}.edu-main{page-break-before:always}} .profile-container{height:170px;width:170px;z-index:2;left:10%}.title{position:absolute;margin-left:calc(100px);width:calc(100% - 100px);padding:1em;margin-top:-240px;z-index:0}.title>*{width:100%;margin-bottom:0}img.profile{left:10px;width:150px;position:absolute;aspect-ratio:1;transform:scale(1);transition:.5s;z-index:2}img.background2{left:0;width:170px!important;margin-top:-10px;pointer-events:none;z-index:1}img.foreground{border-radius:50%;pointer-events:none;z-index:3}img.background:hover,img.backgroundsml:hover{filter:blur(5px)}.spacer{height:100px}img.profilesml{width:150px;position:absolute;left:50%;margin-left:-85px;aspect-ratio:1;padding-top:calc(var(--s)/5);transform:scale(1);transition:.5s}img.foregroundsml{border-radius:50%;pointer-events:none}img.background2sml{width:170px!important;left:calc(50% - 10px);margin-top:-10px;pointer-events:none;z-index:0}print_text{color:#000!important}@media print{.noprintbreak{page-break-inside:avoid}*{color:#000;background-color:#fff}body{background-color:#fff}.hideprint{display:none}.print_text{color:#000!important}.profile-container{margin-top:10px!important}.r-heading1{font-size:16pt!important;margin-bottom:10px!important}.r-heading2{font-size:14pt!important}.r-heading3{font-size:12pt!important}.r-body,.r-small{font-size:10pt!important}.spacer{height:25px!important}}.r-heading1{margin-bottom:20px}.r-heading2{margin-bottom:0}.r-heading3{margin-bottom:.5em}@media (max-width:500px){.print_text{font-size:10px}}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{text-transform:none}

View File

@@ -1 +1 @@
.social-icons{color:#313437;background-color:#fff;padding:70px 0}@media (max-width:767px){.social-icons{padding:50px 0}}@media (max-width:500px){img.profile{width:200px;margin-left:-100px}.profile-container{height:200px;margin-top:2em!important}}.social-div{display:flex;justify-content:center;align-items:center}.social-list{display:flex;list-style:none;gap:2.5rem}.social-icons i{color:#757980;margin:0 10px;width:60px;height:60px;border:1px solid #c8ced7;text-align:center;border-radius:50%;line-height:60px;display:inline-block}.social-link a{text-decoration:none;width:4.8rem;height:4.8rem;background-color:#f0f9fe;border-radius:50%;display:flex;justify-content:center;align-items:center;position:relative;z-index:1;border:3px solid #f0f9fe;overflow:hidden}.social-link a::before{content:"";position:absolute;width:100%;height:100%;background:var(--bg-color);z-index:0;scale:1 0;transform-origin:bottom;transition:scale .5s}.social-link:hover a::before{scale:1 1}.icon{font-size:2rem;color:#011827;transition:.5s;z-index:2}.social-link a:hover .icon{color:#fff;transform:rotateY(360deg)}.social-link{--bg-color:#000}.social-link.discord{--bg-color:#5865f2}.social-link.mastodon{--bg-color:#6364ff}.social-link.youtube{--bg-color:#ff0000}.social-link.signal{--bg-color:#365eb6} :root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}@media (min-width:992px){.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}}

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

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

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

View File

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

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

@@ -0,0 +1 @@
!function(){"use strict";var e=document.querySelector("#mainNav");if(e){var o=e.querySelector(".navbar-collapse");if(o){var n=new bootstrap.Collapse(o,{toggle:!1}),t=o.querySelectorAll("a");for(var a of t)a.addEventListener("click",(function(e){n.hide()}))}var r=function(){(void 0!==window.pageYOffset?window.pageYOffset:(document.documentElement||document.body.parentNode||document.body).scrollTop)>100?e.classList.add("navbar-shrink"):e.classList.remove("navbar-shrink")};r(),document.addEventListener("scroll",r)}}();

View File

@@ -1 +1 @@
const letters="ABCDEFGHIJKLMNOPQRSTUVWXYZ/.?!@#$%^&*()_+";let interval=null,interval2=null,interval3=null;document.querySelector(".copyright").onmouseover=t=>{let e=0,l="Copyright © Nathan Woodburn 2023";clearInterval(interval2),interval2=setInterval((()=>{t.target.innerText=t.target.innerText.split("").map(((t,r)=>r<e?l[r]:letters[Math.floor(41*Math.random())])).join(""),e>=l.length&&clearInterval(interval2),e+=1/3}),10)}; const letters="ABCDEFGHIJKLMNOPQRSTUVWXYZ/.?!@#$%^&*()_+";let interval=null,interval2=null,interval3=null;document.querySelector(".copyright").onmouseover=t=>{let e=0,l="Copyright © Nathan.Woodburn/ 2024";clearInterval(interval2),interval2=setInterval((()=>{t.target.innerText=t.target.innerText.split("").map(((t,r)=>r<e?l[r]:letters[Math.floor(41*Math.random())])).join(""),e>=l.length&&clearInterval(interval2),e+=1/3}),10)};

View File

@@ -1 +1 @@
const letters="ABCDEFGHIJKLMNOPQRSTUVWXYZ/.?!@#$%^&*()_+";let interval=null,interval2=null,interval3=null;window.onload=t=>{target=document.querySelector(".nathanwoodburn");let e=0,n="NATHAN.WOODBURN/";clearInterval(interval),interval=setInterval((()=>{target.innerText=target.innerText.split("").map(((t,r)=>r<e?n[r]:letters[Math.floor(41*Math.random())])).join(""),e>=n.length&&clearInterval(interval),e+=1/3}),30)},document.querySelector(".copyright").onmouseover=t=>{let e=0,n="Copyright © Nathan.Woodburn/ 2024";console.log(n),clearInterval(interval2),interval2=setInterval((()=>{t.target.innerText=t.target.innerText.split("").map(((t,r)=>r<e?n[r]:letters[Math.floor(41*Math.random())])).join(""),e>=n.length&&clearInterval(interval2),e+=1/3}),10)}; const letters="ABCDEFGHIJKLMNOPQRSTUVWXYZ/.?!@#$%^&*()_+";let interval=null,interval2=null,interval3=null;window.onload=t=>{if(target=document.querySelector(".nathanwoodburn"),target){let t=0,e="NATHAN.WOODBURN/";clearInterval(interval),interval=setInterval((()=>{target.innerText=target.innerText.split("").map(((r,n)=>n<t?e[n]:letters[Math.floor(41*Math.random())])).join(""),t>=e.length&&clearInterval(interval),t+=1/3}),30)}},document.querySelector(".copyright").onmouseover=t=>{let e=0,r="Copyright © Nathan.Woodburn/ 2025";console.log(r),clearInterval(interval2),interval2=setInterval((()=>{t.target.innerText=t.target.innerText.split("").map(((t,n)=>n<e?r[n]:letters[Math.floor(41*Math.random())])).join(""),e>=r.length&&clearInterval(interval2),e+=1/3}),10)};

View File

@@ -1 +1 @@
document.addEventListener("DOMContentLoaded",(function(){const n=document.getElementById("loading-screen"),e=[{pre:'┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~</span>]',message:"cd Git"},{pre:'┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~/Git</span>]',message:"cd Nathanwoodburn.github.io"},{pre:'┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~/Git/Nathanwoodburn.github.io</span>]',message:"python3 main.py"}],t=["Starting server with 1 workers and 2 threads","+0000] [1] [INFO] Starting gunicorn 22.0.0","+0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)","+0000] [1] [INFO] Using worker: gthread","+0000] [8] [INFO] Booting worker with pid: 8"];let a=0;function s(e,t){const a=function(){const n=new Date;return`${n.getUTCFullYear()}-${String(n.getUTCMonth()+1).padStart(2,"0")}-${String(n.getUTCDate()).padStart(2,"0")} ${String(n.getUTCHours()).padStart(2,"0")}:${String(n.getUTCMinutes()).padStart(2,"0")}:${String(n.getUTCSeconds()).padStart(2,"0")}`}();console.log(a);for(let t=0;t<e.length;t++){const s=document.createElement("div");s.classList.add("loading-line"),s.innerHTML=0!==t?"["+a+e[t]:e[t],n.appendChild(s)}t()}function r(){window.location.reload()}!function i(){a<e.length?function(e,t){const a=document.createElement("div");a.classList.add("loading-pre"),a.innerHTML=e.pre,n.appendChild(a);const s=document.createElement("div");s.classList.add("loading-line"),s.innerHTML='└─<span class="blue">$</span> <span class="cursor"></span>',n.appendChild(s);let r=0;const i=setInterval((()=>{s.removeChild(s.querySelector(".cursor")),s.innerHTML+=e.message[r]+'<span class="cursor"></span>',r++,r===e.message.length&&(s.removeChild(s.querySelector(".cursor")),clearInterval(i),t())}),50)}(e[a],(()=>{a++,setTimeout(i,200)})):s(t,(()=>{setTimeout(r,200)}))}()})); document.addEventListener("DOMContentLoaded",(function(){const s=document.getElementById("loading-screen"),e=[{pre:'┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~</span>]',message:"cd Git"},{pre:'┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~/Git</span>]',message:"cd Nathanwoodburn.github.io"},{pre:'┌──(<span class="blue">nathan@NWTux</span>)-[<span class="white">~/Git/Nathanwoodburn.github.io</span>]',message:"python3 main.py"}],t=["Starting server with 1 workers and 2 threads","+0000] [1] [INFO] Starting gunicorn 22.0.0","+0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)","+0000] [1] [INFO] Using worker: gthread","+0000] [8] [INFO] Booting worker with pid: 8"];let n=0,a=!1,i=!1;function r(){i&&setTimeout(l,200)}function o(e,t){const n=function(){const s=new Date;return`${s.getUTCFullYear()}-${String(s.getUTCMonth()+1).padStart(2,"0")}-${String(s.getUTCDate()).padStart(2,"0")} ${String(s.getUTCHours()).padStart(2,"0")}:${String(s.getUTCMinutes()).padStart(2,"0")}:${String(s.getUTCSeconds()).padStart(2,"0")}`}();for(let t=0;t<e.length;t++){const a=document.createElement("div");a.classList.add("loading-line"),a.innerHTML=0!==t?"["+n+"] "+e[t]:e[t],s.appendChild(a)}i=!0,t()}function l(){"/"===window.location.pathname?window.location.reload():window.location.href="/"}window.addEventListener("keypress",l),window.innerWidth<768&&(console.log("Screen width is less than 768px, allowing click to redirect"),window.addEventListener("click",l)),setTimeout((function(){const s=[{url:"/assets/fonts/fontawesome-all.min.css",type:"style"},{url:"/assets/fonts/font-awesome.min.css",type:"style"},{url:"/assets/fonts/ionicons.min.css",type:"style"},{url:"/assets/fonts/fontawesome5-overrides.min.css",type:"style"},{url:"/assets/css/index.min.css",type:"style"},{url:"/assets/css/swiper.min.css",type:"style"},{url:"/assets/css/animate.min.min.css",type:"style"},{url:"/assets/css/fixes.min.css",type:"style"},{url:"/assets/css/Footer-Dark-icons.min.css",type:"style"},{url:"/assets/css/GridSystem-1.min.css",type:"style"},{url:"/assets/js/hacker.min.js",type:"script"},{url:"/assets/js/downtime.min.js",type:"script"},{url:"/assets/js/pfp.min.js",type:"script"},{url:"/assets/js/sites.min.js",type:"script"},{url:"/assets/img/pfront.webp",type:"image"},{url:"/assets/img/profile.jpg",type:"image"},{url:"/assets/img/tilt.svg",type:"image"},{url:"/assets/img/bg/BlueMountains.jpg",type:"image"},{url:"/assets/img/wavesblack.svg",type:"image"}];let e=0;const t=s.length;function n(){e++,e===t&&(a=!0,r())}s.forEach((s=>{const e=document.createElement("link");e.rel="preload",e.as=s.type,e.href=s.url,"font"===s.type&&(e.crossOrigin="anonymous"),e.onload=n,e.onerror=n,document.head.appendChild(e)}))}),100),function a(){n<e.length?function(e,t){const n=document.createElement("div");n.classList.add("loading-pre"),n.innerHTML=e.pre,s.appendChild(n);const a=document.createElement("div");a.classList.add("loading-line"),a.innerHTML='└─<span class="blue">$</span> <span class="cursor"></span>',s.appendChild(a);let i=0;const r=setInterval((()=>{a.removeChild(a.querySelector(".cursor")),a.innerHTML+=e.message[i]+'<span class="cursor"></span>',i++,i===e.message.length&&(a.removeChild(a.querySelector(".cursor")),clearInterval(r),t())}),50)}(e[n],(()=>{n++,setTimeout(a,200)})):o(t,(()=>{r()}))}()}));

View File

@@ -1 +1 @@
window.innerWidth<768&&[].slice.call(document.querySelectorAll("[data-bss-disabled-mobile]")).forEach((function(e){e.classList.remove("animated"),e.removeAttribute("data-bss-hover-animate"),e.removeAttribute("data-aos"),e.removeAttribute("data-bss-parallax-bg"),e.removeAttribute("data-bss-scroll-zoom")})),document.addEventListener("DOMContentLoaded",(function(){[].slice.call(document.querySelectorAll("[data-bss-hover-animate]")).forEach((function(e){e.addEventListener("mouseenter",(function(e){e.target.classList.add("animated",e.target.dataset.bssHoverAnimate)})),e.addEventListener("mouseleave",(function(e){e.target.classList.remove("animated",e.target.dataset.bssHoverAnimate)}))})),[].slice.call(document.querySelectorAll("[data-bss-tooltip]")).map((function(e){return new bootstrap.Tooltip(e)}))}),!1),function(){"use strict";var e=document.querySelector("#mainNav");if(e){var t=e.querySelector(".navbar-collapse");if(t){var a=new bootstrap.Collapse(t,{toggle:!1}),o=t.querySelectorAll("a");for(var n of o)n.addEventListener("click",(function(e){a.hide()}))}var r=function(){(void 0!==window.pageYOffset?window.pageYOffset:(document.documentElement||document.body.parentNode||document.body).scrollTop)>100?e.classList.add("navbar-shrink"):e.classList.remove("navbar-shrink")};r(),document.addEventListener("scroll",r)}}(); window.innerWidth<768&&[].slice.call(document.querySelectorAll("[data-bss-disabled-mobile]")).forEach((function(e){e.classList.remove("animated"),e.removeAttribute("data-bss-hover-animate"),e.removeAttribute("data-aos"),e.removeAttribute("data-bss-parallax-bg"),e.removeAttribute("data-bss-scroll-zoom")})),document.addEventListener("DOMContentLoaded",(function(){[].slice.call(document.querySelectorAll("[data-bss-hover-animate]")).forEach((function(e){e.addEventListener("mouseenter",(function(e){e.target.classList.add("animated",e.target.dataset.bssHoverAnimate)})),e.addEventListener("mouseleave",(function(e){e.target.classList.remove("animated",e.target.dataset.bssHoverAnimate)}))})),[].slice.call(document.querySelectorAll("[data-bss-tooltip]")).map((function(e){return new bootstrap.Tooltip(e)}))}),!1);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,16 @@
{
"name": "Woodburn Vault",
"symbol": "stWDBRN",
"image": "https://nathan.woodburn.au/stWDBRN.png",
"description": "Woodburn Vault Token",
"extensions": {
"website": "https://nathan.woodburn.au"
},
"tags": [
"Woodburn"
],
"creator": {
"name": "Nathan.Woodburn/",
"site": "https://nathan.woodburn.au"
}
}

155
templates/blog/blog.html Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More