Compare commits
231 Commits
develop
...
c15a5d5a8b
| Author | SHA1 | Date | |
|---|---|---|---|
|
c15a5d5a8b
|
|||
|
720e59c144
|
|||
|
c45a30675c
|
|||
|
9e8e23165e
|
|||
|
53e05922bf
|
|||
|
0be0dad1b2
|
|||
|
f404d55935
|
|||
|
009c2b430c
|
|||
|
ed96fbcc29
|
|||
|
4555ef5da2
|
|||
|
1335a73eb6
|
|||
|
b8f3039629
|
|||
|
1888160fa5
|
|||
|
7dd0f839cf
|
|||
|
5a0068586a
|
|||
|
8079780c08
|
|||
|
72b8dae35e
|
|||
|
323ace5775
|
|||
|
c2803e372a
|
|||
|
2a9e704f29
|
|||
|
0c490625a9
|
|||
|
b9753617ad
|
|||
|
b87d19c5d9
|
|||
|
67e8b4cf7e
|
|||
|
bfc6652f29
|
|||
|
38372c0cff
|
|||
|
dd64313006
|
|||
|
9e20a6171a
|
|||
|
da347fd860
|
|||
|
776b7de753
|
|||
|
7b2b3659bb
|
|||
|
872373dffd
|
|||
|
8d832372cd
|
|||
|
03dae87272
|
|||
|
4c654fcb78
|
|||
|
c9542e4af7
|
|||
|
e184375897
|
|||
|
844f1b52e2
|
|||
|
19c51c3665
|
|||
|
85ebd460ed
|
|||
|
50879b4f0e
|
|||
|
6c09923281
|
|||
|
332c408b89
|
|||
|
4ae6f7bb99
|
|||
|
d4d6b47225
|
|||
|
9809fe0695
|
|||
|
3522389422
|
|||
|
2979d3c4de
|
|||
|
a8b2c02164
|
|||
|
372ba908b8
|
|||
|
1145b9205c
|
|||
|
a71c5b6663
|
|||
|
724e800201
|
|||
|
abcaa9283d
|
|||
|
e175f68d25
|
|||
|
80b6a9bf46
|
|||
|
b089b8c0a8
|
|||
|
8f774ba8f0
|
|||
|
f4f5f47ee7
|
|||
|
16f17a9486
|
|||
|
72483674f6
|
|||
|
b69c7f381b
|
|||
|
d7d4dbed8b
|
|||
|
2437b19836
|
|||
|
abd23e0eb8
|
|||
|
57a4b977ec
|
|||
|
7f591e2724
|
|||
|
3d5c16f9cb
|
|||
|
fdb5f84c92
|
|||
|
eaf363ee27
|
|||
|
0ea9db3473
|
|||
|
8d6acca5e9
|
|||
|
bfc1f0839a
|
|||
|
258061c64d
|
|||
|
399ac5f0da
|
|||
|
74362de02a
|
|||
|
9f7b93b8a1
|
|||
|
665921d046
|
|||
|
84cf772273
|
|||
|
22cd49a012
|
|||
|
00d035a0e8
|
|||
|
fc56cafab8
|
|||
|
eee87e6ca7
|
|||
|
09852f19b6
|
|||
|
8b464cd89d
|
|||
|
98597768f3
|
|||
|
08f80ddb5c
|
|||
|
33fd8136a7
|
|||
|
b2943bfeac
|
|||
|
c3c7c86a66
|
|||
|
8563a6857f
|
|||
|
1650d25d0f
|
|||
|
f4ee2297a7
|
|||
|
35ced02977
|
|||
|
45709632d5
|
|||
|
e65fe8cd30
|
|||
|
21f725baf3
|
|||
|
c2a1995292
|
|||
|
af93330bf5
|
|||
|
e43ae63d8a
|
|||
|
add64ba889
|
|||
|
8d13297cb0
|
|||
|
eee95df47c
|
|||
|
1febeaf8a3
|
|||
|
088be26b48
|
|||
|
7f055f3607
|
|||
|
5a455fcdca
|
|||
|
846e28d92f
|
|||
|
ec3563093f
|
|||
|
0ec96b4461
|
|||
|
873504ecbf
|
|||
|
51f4a3462b
|
|||
|
0656d8a95d
|
|||
|
a79c795672
|
|||
|
159a40ecb5
|
|||
|
9d359640d3
|
|||
|
f0c60a4cea
|
|||
|
2c5ac7b475
|
|||
|
6a64de155d
|
|||
|
f5ba80fc16
|
|||
|
f8f48e9353
|
|||
|
f734e9ade4
|
|||
|
95e161aba9
|
|||
|
e4331e712a
|
|||
|
4804016fdf
|
|||
|
ccb6cdb454
|
|||
|
dade1f5d8d
|
|||
|
cecfdea07f
|
|||
|
7bea502bdf
|
|||
|
1fd539bcb9
|
|||
|
f940f4418d
|
|||
|
de476eb7ab
|
|||
|
2fbf21eaaf
|
|||
|
8ccbe2ebf1
|
|||
|
d4136a396a
|
|||
|
622a4038ac
|
|||
|
a65896b80d
|
|||
|
415703e21b
|
|||
|
89bf5eecd7
|
|||
|
b4059910ec
|
|||
|
7896f71534
|
|||
|
f9b30a3fe6
|
|||
|
31c80276de
|
|||
|
840ba4c10c
|
|||
|
53bf28b208
|
|||
|
d651e3a20c
|
|||
|
fb376a4906
|
|||
|
cd69d86246
|
|||
|
1ae1eeb159
|
|||
|
5171fffab5
|
|||
|
ca1e99013d
|
|||
|
7c71c994f2
|
|||
|
70655a4d39
|
|||
|
1551799b88
|
|||
|
4ab0db973f
|
|||
|
a3a2748d15
|
|||
|
ce8897d578
|
|||
|
637562f920
|
|||
|
42aff1f455
|
|||
|
855f6b3c99
|
|||
|
8a50ae7e32
|
|||
|
1a3572e64c
|
|||
|
ad6e3fe9d8
|
|||
|
99b63592d0
|
|||
|
6c004d14bd
|
|||
|
a6b488d1a6
|
|||
|
0cef259ecc
|
|||
|
150bc17ed4
|
|||
|
c9e9c9be46
|
|||
|
90d8dc3428
|
|||
|
863d11cffd
|
|||
|
b3d965d220
|
|||
|
496205c6b4
|
|||
|
9552df4b4e
|
|||
|
9881584cdb
|
|||
|
867583d30c
|
|||
|
125768b01e
|
|||
|
320538ad17
|
|||
|
56a5539106
|
|||
|
56b1048622
|
|||
|
3e4d5c2633
|
|||
|
cdb7eb86a5
|
|||
|
6de2518f1f
|
|||
|
34580eeab4
|
|||
|
0dd03e544c
|
|||
|
84a6310189
|
|||
|
68f8c55817
|
|||
|
9c0d592a24
|
|||
|
56349561f9
|
|||
|
702282d11f
|
|||
|
46856a9399
|
|||
|
eecf9b8db8
|
|||
|
1c891d971f
|
|||
|
6dfa664807
|
|||
|
5a27be5a3c
|
|||
|
8687daecfd
|
|||
|
6b5fe4bc2f
|
|||
|
c86bae36ad
|
|||
|
ef40e8078c
|
|||
|
80fc1cdc4d
|
|||
|
331de40dfd
|
|||
|
f3ee6607e7
|
|||
|
eec66b13ca
|
|||
|
9fc218feb1
|
|||
|
9ed68d1f0b
|
|||
|
80cc8022eb
|
|||
|
6bf45b9c2b
|
|||
|
860d070c55
|
|||
|
7e49d23736
|
|||
|
69fb34c2ba
|
|||
|
6664de6c07
|
|||
|
b93515ff32
|
|||
|
230042cc7b
|
|||
|
98acc8543c
|
|||
|
eea21cea1e
|
|||
|
fb8136f3b6
|
|||
|
e84c39030d
|
|||
|
75308cb264
|
|||
|
9b81a0bf18
|
|||
|
005a306fc6
|
|||
|
cb13ca0a3b
|
|||
|
5685830cba
|
|||
|
1f9b38306c
|
|||
|
9568cfe177
|
|||
|
18619efe39
|
|||
|
719221d74f
|
|||
|
5d95307ae2
|
|||
|
1b017d919a
|
|||
|
ce5ec9aace
|
|||
|
668dc8683b
|
|||
|
26c91c030a
|
33
.dockerignore
Normal 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
|
||||||
|
|
||||||
|
|
||||||
18
.gitea/workflows/check.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
name: Check Code Quality
|
||||||
|
run-name: Ruff CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
RuffCheck:
|
||||||
|
runs-on: [ubuntu-latest, amd]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
run: |
|
||||||
|
apt update
|
||||||
|
apt install -y python3 python3-pip
|
||||||
|
- name: Install Ruff
|
||||||
|
run: pip install ruff
|
||||||
|
- name: Run Ruff
|
||||||
|
run: ruff check .
|
||||||
3
.gitignore
vendored
@@ -3,3 +3,6 @@ __pycache__/
|
|||||||
|
|
||||||
.env
|
.env
|
||||||
.vs/
|
.vs/
|
||||||
|
.venv/
|
||||||
|
*.tmp
|
||||||
|
testing/
|
||||||
|
|||||||
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
8
.well-known/assetlinks.json
Normal 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"]
|
||||||
|
}
|
||||||
|
}]
|
||||||
6
.well-known/wallets/.BTC.proof
Normal 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.
|
||||||
6
.well-known/wallets/.ETH.proof
Normal 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
|
||||||
13
.well-known/wallets/.SOL.proof
Normal 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.
|
||||||
@@ -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)"
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1 +1 @@
|
|||||||
addr1qyudjff2lfuxjjjfz0y542drw4srxq2xxtgrq22vm8ymvwwagq028esaz4rsexct8zzsuds29e03eaykahlvhdx0k9es4gnk27
|
addr1qy5l7vmx9l2uexv44hzjak4zmwecee4hht0k6shtk5jh7cjzu99z8vx5n467fquzradx7p42grdylv3zq2cgfw0f32fs443hxs
|
||||||
1
.well-known/wallets/APT
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0x372b3c513d149e5511912eba22e31f07d2b289e20ba84b2e0b7756e7a00295c3
|
||||||
1
.well-known/wallets/BCH
Normal file
@@ -0,0 +1 @@
|
|||||||
|
qpsgs9daa6e2mn4v0u02pfunsme68a5uayn7e8knug
|
||||||
@@ -1 +1 @@
|
|||||||
bc1qhs94zzcw64qnwq4hvk056rwxwvgrkd7tq7d4xw
|
bc1qzsp3wc2nwayl8awun57tggjn92w0awp9mzsn90
|
||||||
1
.well-known/wallets/DASH
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Xpr5auWs1waBmWT3XsWXwzu8Di32x8VfH2
|
||||||
@@ -1 +1 @@
|
|||||||
hs1qk4sq6mk3kcshp02xgchukv09m38czdnq5qv76w
|
hs1qh7uzytf2ftwkd9dmjjs7az9qfver5m7dd7x4ej
|
||||||
1
.well-known/wallets/KAS
Normal file
@@ -0,0 +1 @@
|
|||||||
|
kaspa:qzl7av7gq5j594pcs2gn6zf2xadpmhdm90nygjstvte0n6gt9f4fgx0w2dhm8
|
||||||
1
.well-known/wallets/NOBLE
Normal file
@@ -0,0 +1 @@
|
|||||||
|
noble1ugraczuyfmxy8k38nps4fu7e5derryzxywjul2
|
||||||
@@ -1 +1 @@
|
|||||||
UQAtpYb-yOaTSA40mJRvUxKv1WVeIPi-g_4Jqn0oWYypsQIf
|
UQDqC1B0a3S9th8ncaQHYJ689dnu9c0zJXeV727UMak9WbBm
|
||||||
|
|||||||
1
.well-known/wallets/TRX
Normal file
@@ -0,0 +1 @@
|
|||||||
|
THjwavxGZahj1scVw75fhGP2HCAcjNxwsK
|
||||||
1
.well-known/wallets/XLM
Normal file
@@ -0,0 +1 @@
|
|||||||
|
GCK4PA53V26MNP6U57EPK7EA42TBQMGJ4TUPMUPLLQNPZ64YX3XVLZGQ
|
||||||
12
.well-known/xrp-ledger.toml
Normal 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"
|
||||||
65
Dockerfile
@@ -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 what’s 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
35
addCoin.py
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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"[1m{post['header']}[0m\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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,132 @@
|
|||||||
|
from flask import render_template
|
||||||
|
from tools import getAddress, get_tools_data, getClientIP
|
||||||
|
import os
|
||||||
|
from functools import lru_cache
|
||||||
|
import requests
|
||||||
|
from blueprints.spotify import get_spotify_track
|
||||||
|
|
||||||
|
|
||||||
|
MAX_WIDTH = 80
|
||||||
|
|
||||||
|
def clean_path(path:str):
|
||||||
|
path = path.strip("/ ").lower()
|
||||||
|
# Strip any .html extension
|
||||||
|
if path.endswith(".html"):
|
||||||
|
path = path[:-5]
|
||||||
|
|
||||||
|
# If the path is empty, set it to "index"
|
||||||
|
if path == "":
|
||||||
|
path = "index"
|
||||||
|
return path
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_header():
|
||||||
|
with open("templates/header.ascii", "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_current_project():
|
||||||
|
git = requests.get(
|
||||||
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/activities/feeds?only-performed-by=true&limit=1",
|
||||||
|
headers={"Authorization": os.getenv("GIT_AUTH") if os.getenv("GIT_AUTH") else os.getenv("git_token")},
|
||||||
|
)
|
||||||
|
git = git.json()
|
||||||
|
git = git[0]
|
||||||
|
repo_name = git["repo"]["name"]
|
||||||
|
repo_name = repo_name.lower()
|
||||||
|
repo_description = git["repo"]["description"]
|
||||||
|
if not repo_description:
|
||||||
|
return f"[1;36m{repo_name}[0m"
|
||||||
|
return f"[1;36m{repo_name}[0m - [1m{repo_description}[0m"
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_projects():
|
||||||
|
projectsreq = requests.get(
|
||||||
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos"
|
||||||
|
)
|
||||||
|
|
||||||
|
projects = projectsreq.json()
|
||||||
|
|
||||||
|
# Check for next page
|
||||||
|
pageNum = 1
|
||||||
|
while 'rel="next"' in projectsreq.headers["link"]:
|
||||||
|
projectsreq = requests.get(
|
||||||
|
"https://git.woodburn.au/api/v1/users/nathanwoodburn/repos?page="
|
||||||
|
+ str(pageNum)
|
||||||
|
)
|
||||||
|
projects += projectsreq.json()
|
||||||
|
pageNum += 1
|
||||||
|
|
||||||
|
# Sort by last updated
|
||||||
|
projectsList = sorted(
|
||||||
|
projects, key=lambda x: x["updated_at"], reverse=True)
|
||||||
|
projects = ""
|
||||||
|
projectNum = 0
|
||||||
|
includedNames = []
|
||||||
|
while len(includedNames) < 5 and projectNum < len(projectsList):
|
||||||
|
# Avoid duplicates
|
||||||
|
if projectsList[projectNum]["name"] in includedNames:
|
||||||
|
projectNum += 1
|
||||||
|
continue
|
||||||
|
includedNames.append(projectsList[projectNum]["name"])
|
||||||
|
project = projectsList[projectNum]
|
||||||
|
projects += f"""[1m{project['name']}[0m - {project['description'] if project['description'] else 'No description'}
|
||||||
|
{project['html_url']}
|
||||||
|
|
||||||
|
"""
|
||||||
|
projectNum += 1
|
||||||
|
|
||||||
|
return projects
|
||||||
|
|
||||||
|
def curl_response(request):
|
||||||
|
# Check if <path>.ascii exists
|
||||||
|
path = clean_path(request.path)
|
||||||
|
|
||||||
|
# Handle special cases
|
||||||
|
if path == "index":
|
||||||
|
# Get current project
|
||||||
|
return render_template("index.ascii",repo=get_current_project(), ip=getClientIP(request), spotify=get_spotify_track()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
if path == "projects":
|
||||||
|
# Get projects
|
||||||
|
return render_template("projects.ascii",header=get_header(),projects=get_projects()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
|
if path == "donate":
|
||||||
|
# Get donation info
|
||||||
|
return render_template("donate.ascii",header=get_header(),
|
||||||
|
HNS=getAddress("HNS"), BTC=getAddress("BTC"),
|
||||||
|
SOL=getAddress("SOL"), ETH=getAddress("ETH")
|
||||||
|
), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
|
if path == "donate/more":
|
||||||
|
coinList = os.listdir(".well-known/wallets")
|
||||||
|
coinList = [file for file in coinList if file[0] != "."]
|
||||||
|
coinList.sort()
|
||||||
|
return render_template("donate_more.ascii",header=get_header(),
|
||||||
|
coins=coinList
|
||||||
|
), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
|
# For other donation pages, fall back to ascii if it exists
|
||||||
|
if path.startswith("donate/"):
|
||||||
|
coin = path.split("/")[1]
|
||||||
|
address = getAddress(coin)
|
||||||
|
if address != "":
|
||||||
|
return render_template("donate_coin.ascii",header=get_header(),coin=coin.upper(),address=address), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
|
if path == "tools":
|
||||||
|
tools = get_tools_data()
|
||||||
|
return render_template("tools.ascii",header=get_header(),tools=tools), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
|
if os.path.exists(f"templates/{path}.ascii"):
|
||||||
|
return render_template(f"{path}.ascii",header=get_header()), 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
|
|
||||||
|
# Fallback to html if it exists
|
||||||
|
if os.path.exists(f"templates/{path}.html"):
|
||||||
|
return render_template(f"{path}.html")
|
||||||
|
|
||||||
|
# Return curl error page
|
||||||
|
error = {
|
||||||
|
"code": 404,
|
||||||
|
"message": "The requested resource was not found on this server."
|
||||||
|
}
|
||||||
|
return render_template("error.ascii",header=get_header(),error=error), 404, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||||
27
data/blog/Fingertip_on_Linux_Mint.md
Normal 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.
|
||||||
28
data/blog/Nameserver_Setup_On_BobWallet.md
Normal 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)
|
||||||
53
data/blog/Software_I_Use.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
G'day,
|
||||||
|
Just thought it might be useful to write down some of the software I use regularly. I've no clue if you'll find any useful :)
|
||||||
|
|
||||||
|
For a more complete list, check out [/tools](/tools)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
## Overview
|
||||||
|
OS: Arch Linux | Because it is quick to update and has all the latest tools I can play with
|
||||||
|
DE: Hyprland | Feel free to check out my dotfiles if you're interested
|
||||||
|
Shell: ZSH
|
||||||
|
|
||||||
|
<br>
|
||||||
|
## Desktop Applications
|
||||||
|
|
||||||
|
[Obsidian](https://obsidian.md/) | Note taking app that stores everything in Markdown files
|
||||||
|
[Alacritty](https://alacritty.org/) | Terminal emulator
|
||||||
|
[Brave](https://brave.com/) | Browser with ad blocker built in
|
||||||
|
[VSCode](https://code.visualstudio.com/) | Yeah its heavy but I'm used to it
|
||||||
|
|
||||||
|
<br>
|
||||||
|
## Terminal Tools
|
||||||
|
|
||||||
|
[Zellij](https://zellij.dev/) | Easy to use terminal multiplexer
|
||||||
|
[Fx](https://fx.wtf/) | JSON parser with pretty colours. Similar to jq
|
||||||
|
[Zoxide](https://github.com/ajeetdsouza/zoxide) | cd but with fuzzy matching and other cool features
|
||||||
|
[Atuin](https://atuin.sh/) | Terminal history with fuzzy search
|
||||||
|
[Tmate](https://tmate.io/) | Terminal sharing. Useful when troubleshooting isses for remote users
|
||||||
|
[Eza](https://eza.rocks/) | Like ls but pretty
|
||||||
|
[Tre](https://github.com/dduan/tre) | Like tree but pretty
|
||||||
|
[Bat](https://github.com/sharkdp/bat) | Like cat but pretty. Syntax highlighting, line numbers, search, git integration and more
|
||||||
|
[Oh My ZSH](https://ohmyz.sh/) | Shell customization and plugins
|
||||||
|
|
||||||
|
<br>
|
||||||
|
## Server Management
|
||||||
|
|
||||||
|
[Proxmox](https://proxmox.com/en/) | Virtualization manager for my baremetal server
|
||||||
|
[Portainer](https://www.portainer.io/) | Docker container manager
|
||||||
|
[Coolify](https://coolify.io/) | Open source alternative to heroku. I use it to host a lot of different services
|
||||||
|
[Opnsense](https://opnsense.org/) | Firewall and router
|
||||||
|
[Nginx Proxy Manager](https://nginxproxymanager.com/) | Reverse proxy manager with a nice UI
|
||||||
|
[Tailscale](https://tailscale.com/) | VPN to let me access my network from anywhere
|
||||||
|
|
||||||
|
<br>
|
||||||
|
## Self-Hosting Services
|
||||||
|
[Authentik](https://goauthentik.io/) | Identity provider for single sign on
|
||||||
|
[Gitea](https://gitea.io/) | Git hosting service
|
||||||
|
[Nextcloud](https://nextcloud.com/) | Think Dropbox but self hosted
|
||||||
|
[Umami](https://umami.is/) | Self hosted web analytics
|
||||||
|
[Uptime Kuma](https://uptime.kuma.pet/) | Self hosted status page and monitoring tool
|
||||||
|
[PhotoPrism](https://photoprism.app/) | Self hosted photo management tool
|
||||||
|
[FreeScout](https://freescout.net/) | Self hosted email dashboard
|
||||||
|
[Transfer.sh](https://upload.woodburn.au/) | Self hosted file sharing service
|
||||||
|
|
||||||
77
data/nathanwoodburn.asc
Normal 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
BIN
data/resume_support.pdf
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||||
|
]
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
flask
|
|
||||||
Flask-Cors
|
|
||||||
python-dotenv
|
|
||||||
gunicorn
|
|
||||||
requests
|
|
||||||
cloudflare
|
|
||||||
qrcode
|
|
||||||
ansi2html
|
|
||||||
53
templates/403.html
Normal 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&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap">
|
||||||
|
<link rel="stylesheet" href="/assets/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: <span>403</span></p>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-start" style="display: inline-block;"><code><em>this_page</em>.<em>found</em> = true;</code><code><span>if</span> (<em>this_page</em>.<em>readable</em>) {<br><span class="tab-space"></span><b>return</b> <em>this_page</em>;<br>} <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>
|
||||||
@@ -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&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&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&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>
|
||||||
|
|
||||||
|
|||||||
@@ -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&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&display=swap">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&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&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>
|
||||||
|
|||||||
1
templates/assets/css/Social-Icons.min.css
vendored
Normal 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
@@ -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}
|
||||||
1
templates/assets/css/brand-reveal.min.css
vendored
Normal 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}.hr-l{width:80%;border-width:2px;border-color:var(--bs-light);margin-top:0;opacity:.8}.hr-l-primary{border-width:3px;border-color:var(--bs-primary);margin-top:0;opacity:1}.float-right{position:absolute;right:3em}
|
||||||
2
templates/assets/css/index.min.css
vendored
@@ -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}
|
||||||
2
templates/assets/css/profile.min.css
vendored
@@ -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%}
|
||||||
104
templates/assets/css/resume-print.css
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/* print.css */
|
||||||
|
@media print {
|
||||||
|
|
||||||
|
/* Page margins */
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 10mm 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset body */
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 10pt; /* smaller for print */
|
||||||
|
line-height: 1.3;
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container adjustments */
|
||||||
|
.container-fluid, .resume-row, .resume-column {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* background: none !important; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flex layout for 33/66 split */
|
||||||
|
.resume-row {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resume-column-left {
|
||||||
|
flex: 0 0 33.3333% !important;
|
||||||
|
max-width: 33.3333% !important;
|
||||||
|
padding-left: 5mm !important;
|
||||||
|
padding-right: 5mm !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border: none !important;
|
||||||
|
break-inside: avoid !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
.resume-column-left a {
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resume-column-right {
|
||||||
|
flex: 0 0 66.6667% !important;
|
||||||
|
max-width: 66.6667% !important;
|
||||||
|
padding-left: 5mm !important;
|
||||||
|
padding-right: 5mm !important;
|
||||||
|
background: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
border: none !important;
|
||||||
|
break-inside: avoid !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Images adjustments */
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
display: block;
|
||||||
|
margin: 10mm auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text adjustments for print */
|
||||||
|
h1 { font-size: 14pt; margin-bottom: 3mm; }
|
||||||
|
h2 { font-size: 12pt; margin-bottom: 2mm; }
|
||||||
|
h3 { font-size: 11pt; margin-bottom: 2mm; }
|
||||||
|
h4 { font-size: 10pt; margin-bottom: 1mm; }
|
||||||
|
h5, h6 { font-size: 9pt; margin-bottom: 1mm; }
|
||||||
|
p, li, .r-body, .l-body { font-size: 10pt; line-height: 1.3; }
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 36px !important;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.r-heading1 {
|
||||||
|
margin-top: 4mm !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links as plain text */
|
||||||
|
a {
|
||||||
|
color: #000 !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avoid page breaks inside blocks */
|
||||||
|
.noprintbreak {
|
||||||
|
break-inside: avoid !important;
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
/* margin-bottom: 5mm !important; */
|
||||||
|
}
|
||||||
|
.r-body {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
templates/assets/css/resume.min.css
vendored
@@ -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}}
|
img.profile-side{width:200px;aspect-ratio:1;z-index:2;border:6px solid #fff;margin:3em 0;border-radius:50%}.spacer{height:100px}.l-heading1,.l-heading2,.r-heading2{margin-bottom:0}.l-heading3,.r-heading3{margin-bottom:.5em}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{text-transform:none}.side-column{margin-top:2em}.noprintbreak{margin-bottom:1.5em}.resume-column-left{background:var(--bs-primary);padding-left:3em;padding-right:3em;max-width:320px}.resume-column-right{padding-right:3em;padding-left:3em;background:var(--bs-light);color:var(--bs-black)}.row-fill div{padding:0}.r-heading1{font-size:28px;margin-bottom:0;color:var(--bs-primary)}.title-hr{width:15%;color:var(--bs-primary);border-width:5px;border-color:var(--bs-primary);opacity:1}.l-body{margin-left:1em;line-height:initial}.r-body{line-height:initial}.l-summary{margin-top:3em}::selection{color:#fff;background-color:#0c4279}body{max-width:1400px;margin:0 auto}
|
||||||
2
templates/assets/css/styles.min.css
vendored
@@ -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}
|
||||||
1
templates/assets/css/tools.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.card:hover{transform:translateY(-5px);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);transition:transform .2s,box-shadow .2s}.btn:hover{transform:scale(1.05);transition:transform .2s}
|
||||||
BIN
templates/assets/img/bg/BlueMountains.jpg
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
templates/assets/img/bg/bg_sunset.webp
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
templates/assets/img/bg/sunset.webp
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
templates/assets/img/external/HNS/black/android-chrome-192x192.png
vendored
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
templates/assets/img/external/HNS/black/apple-touch-icon.png
vendored
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
templates/assets/img/external/HNS/black/favicon-16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
templates/assets/img/external/HNS/black/favicon-32x32.png
vendored
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
templates/assets/img/external/HNS/black/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
templates/assets/img/external/HNS/black/favicon.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
templates/assets/img/external/HNS/white/android-chrome-192x192.png
vendored
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
templates/assets/img/external/HNS/white/apple-touch-icon.png
vendored
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
templates/assets/img/external/HNS/white/favicon-16x16.png
vendored
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
templates/assets/img/external/HNS/white/favicon-32x32.png
vendored
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
templates/assets/img/external/HNS/white/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
templates/assets/img/external/HNS/white/favicon.png
vendored
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
templates/assets/img/external/spotify.png
vendored
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
templates/assets/img/external/stWDBRN.png
vendored
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
templates/assets/img/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
templates/assets/img/nathanwoodburn.jpeg
Normal file
|
After Width: | Height: | Size: 560 KiB |
BIN
templates/assets/img/now/24_09_07_term.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
templates/assets/img/now/24_09_07_term.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 12 KiB |
BIN
templates/assets/img/profile.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
templates/assets/img/proof.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
templates/assets/img/signalQR.jpg
Normal file
|
After Width: | Height: | Size: 330 KiB |
1
templates/assets/js/403.min.js
vendored
Normal 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);
|
||||||
@@ -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
@@ -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)}}();
|
||||||
2
templates/assets/js/hacker-podcast.min.js
vendored
@@ -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)};
|
||||||
2
templates/assets/js/hacker.min.js
vendored
@@ -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)};
|
||||||
2
templates/assets/js/loading.min.js
vendored
@@ -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()}))}()}));
|
||||||
2
templates/assets/js/script.min.js
vendored
@@ -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);
|
||||||
|
Before Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 53 KiB |