Compare commits

...

426 Commits

Author SHA1 Message Date
joeac 87527646fa certbot is --non-interactive 2026-07-07 13:29:26 +01:00
joeac 30af54b750 tls before nginx 2026-07-07 13:26:51 +01:00
joeac ac0ebb5302 add actualbudget 2026-07-07 13:25:30 +01:00
joeac 4bf24c2b18 mox_clientsettings: reverse proxy to mox 2026-07-07 13:06:55 +01:00
joeac 20b396effd adds mox nginx configs 2026-07-07 13:03:08 +01:00
joeac b3677dad4c zen, not sbl, .spamhaus.org 2026-07-07 11:13:32 +01:00
joeac b181f59f4c mkdir ~/mox/data if needed 2026-07-07 11:07:53 +01:00
joeac 5d849384bc chown tls_certs 2026-07-07 10:27:10 +01:00
joeac 5e640711ce fix cert-name in tls.mk 2026-07-07 10:22:32 +01:00
joeac 44e4605be8 adds install_tls rule 2026-07-07 10:11:01 +01:00
joeac 60ba3328fa improves certbot cmds 2026-07-07 10:07:17 +01:00
joeac 5a5301fefb fixes certonly command 2026-07-07 10:06:33 +01:00
joeac 4ee7c34161 mox depends on tls certs 2026-07-07 09:53:24 +01:00
joeac fe1d19a9c7 mox/reinstall 2026-07-07 09:49:24 +01:00
joeac 6d8541f052 install tls_domain, not tls_module 2026-07-07 09:48:09 +01:00
joeac ba2359f8b9 again 2026-07-07 09:41:35 +01:00
joeac 9ef09754be fix renew rule call 2026-07-07 09:37:48 +01:00
joeac cbe223166e certbot delete is non-interactive 2026-07-07 09:35:42 +01:00
joeac fb2785f694 again 2026-07-07 09:34:55 +01:00
joeac f9dcf157a0 fix delete_cert for real this time 2026-07-07 09:33:13 +01:00
joeac 83a482de03 fix syntax in delete_cert rule call 2026-07-07 09:30:11 +01:00
joeac 208f383925 if master node, install all subdomains, else none 2026-07-07 09:28:21 +01:00
joeac 05f9535a9b tls.mk: remove 'test' 2026-07-07 09:23:02 +01:00
joeac c18fd9c44e tls.mk: subdomains, not modules 2026-07-07 09:20:06 +01:00
joeac 07fa302d43 tls.mk 2026-07-07 09:10:58 +01:00
joeac 4a2b27f54a microlog post 2026-07-06 2026-07-06 12:58:25 +01:00
joeac ff507ea16a adds conf to mox deps 2026-07-05 23:23:01 +01:00
joeac 639196f0c9 install mox config 2026-07-05 23:18:11 +01:00
joeac 404cb2d98c also dirs != ordinary files 2026-07-05 23:08:02 +01:00
joeac 665d808660 fix case in last two commits 2026-07-05 23:06:29 +01:00
joeac 15027fef29 ditto, and fix dir 2026-07-05 23:05:18 +01:00
joeac 26ae12f638 calcs gemini_user in make, not shell 2026-07-05 23:04:09 +01:00
joeac 56393380b9 only make .env if dir exists 2026-07-05 23:01:56 +01:00
joeac d51d1a1266 adds mox_clientsettings to blade-canongate 2026-07-05 22:59:42 +01:00
joeac af5599ba54 missed a - 2026-07-05 22:58:57 +01:00
joeac ce8017c6d2 remove ref to removed install_dyndns rule 2026-07-05 22:47:45 +01:00
joeac b64f8ddffa replaces - with _ in vars 2026-07-05 22:30:21 +01:00
joeac e926d562c0 adds mox ports 2026-07-05 22:29:03 +01:00
joeac 064adbc0b7 adds mox sub-subdomains 2026-07-05 22:28:53 +01:00
joeac 4872b95c43 cp, not mv, resolv.conf 2026-07-05 16:30:24 +01:00
joeac 4547dd5681 unbound-anchor requires sudo 2026-07-05 16:27:06 +01:00
joeac 655ac1404a fix /etc/resolv.conf.joeac.net-backup rule 2026-07-05 16:24:12 +01:00
joeac 521d768635 adds mox 2026-07-05 16:09:20 +01:00
joeac f736c46b81 defines openrc rules for all modules (so they can be uninstalled) 2026-07-04 20:13:36 +01:00
joeac f537b99d02 uses IMAGE_PREFIX var in gemini.Dockerfile 2026-07-04 20:12:04 +01:00
joeac ad4b03871e moves container.mk vars to vars.mk so that IMAGE_PREFIX is defined
before it is used in COMPOSE_CMD
2026-07-04 20:11:07 +01:00
joeac 4ef4bbd4ad gives gemini pi-broughton -> blade-canongate 2026-07-04 20:02:02 +01:00
joeac 2e44bfa09a remove nginx site requires sudo 2026-07-04 14:25:49 +01:00
joeac 424b4c6971 uses ip addr instead of hostname to simplify dns config 2026-07-04 14:18:42 +01:00
joeac 4130448a55 swaps master node to blade-canongate 2026-07-04 08:43:00 +01:00
joeac ed6a6e5a34 update.sh exports XDG_RUNTIME_DIR 2026-07-03 16:29:17 +01:00
joeac 36ccd48cfc fix dollars in static.conf.template getting removed 2026-07-03 16:23:01 +01:00
joeac 61f2b376ee chown /var dirs 2026-07-03 16:02:05 +01:00
joeac b59888dc99 splits sudo to avoid env confusion 2026-07-03 15:50:14 +01:00
joeac d29e120c72 ...but for real this time 2026-07-03 15:40:15 +01:00
joeac 16cc43ad28 fix bin/pp again? 2026-07-03 15:37:36 +01:00
joeac 3773af632c fix bin/pp rule 2026-07-03 15:34:19 +01:00
joeac 2aeb58d9ed no quotes around COMPOSECMD 2026-07-03 15:23:13 +01:00
joeac affc7521d6 chmod +x for joeac.net openrc service 2026-07-03 15:21:41 +01:00
joeac f5a7d78d4f uses custom COMPOSECMD in joeac.net openrc services 2026-07-03 15:14:02 +01:00
joeac c42c636231 splits sudo to avoid env confusion 2026-07-03 15:08:46 +01:00
joeac 99b21dbe8e stores src in $HOME and more perm tweaks 2026-07-03 15:01:48 +01:00
joeac 3af4546839 adds XDG_RUNTIME_DIR explicitly in update.sh 2026-07-03 13:38:21 +01:00
joeac ace7801796 typo in openrc.mk: joeac.net.$(module) 2026-07-03 13:23:20 +01:00
joeac 7d0a920c8c provides ETHERPAD_VERSION in vars.mk 2026-07-03 12:37:02 +01:00
joeac f24f1ff4b7 provides ALPINE_VERSION in vars.mk 2026-07-03 12:31:49 +01:00
joeac 703359301b envsubst uses -io flags instead of positional params 2026-07-03 12:29:31 +01:00
joeac 66da50eb92 logins back in after updating groups 2026-07-03 12:27:50 +01:00
joeac 757a24894a chmod 770 on /home/joeac.net 2026-07-03 12:20:24 +01:00
joeac 4b19aabba6 chmod on .env 2026-07-03 12:14:02 +01:00
joeac 626642cd25 get_var_value 2026-07-03 12:12:25 +01:00
joeac a1c91da619 set +e install.sh 2026-07-03 12:07:05 +01:00
joeac fac4169389 removes CONTAINER_PREFIX from example.env (redundant) 2026-07-03 12:06:34 +01:00
joeac a5f1ea1358 copes with empty var values 2026-07-03 12:05:45 +01:00
joeac db8df130fe removes ^ from expr for portability 2026-07-03 12:02:38 +01:00
joeac 52f4ab3157 more portable conditon 2026-07-03 12:00:02 +01:00
joeac 8519d854fb expr has : 2026-07-03 11:51:16 +01:00
joeac edd01e4445 absolutely provides dir to make in install.sh 2026-07-03 11:47:05 +01:00
joeac 9ea2820411 absolutises example.env 2026-07-03 11:46:36 +01:00
joeac 967818f315 absolute chown /home/jeoac.net/joeac.net 2026-07-03 11:45:43 +01:00
joeac ca8e06f300 Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-07-03 11:44:50 +01:00
joeac a611b5010f another attempt to fix perms 2026-07-03 11:44:22 +01:00
joeac 2bc353c8fc install.sh gives rw permissions to user over repo using groups 2026-07-03 11:41:00 +01:00
joeac d4dd979311 install.sh gives rw permissions to user over repo using groups 2026-07-03 11:39:27 +01:00
joeac cd4bafbed3 install.sh: no password for joeac.net user 2026-07-03 11:22:56 +01:00
joeac 51d7222c84 install.sh: adds missing joeac/ in repo URL 2026-07-03 11:21:25 +01:00
joeac 888df5c664 install.sh: replaces doas with sudo 2026-07-03 11:20:36 +01:00
joeac 220394ed0d don't set -x in install.sh 2026-07-03 11:19:26 +01:00
joeac 89280d6087 install.sh: wget to /usr requires doas 2026-07-03 11:19:01 +01:00
joeac cb3ad780b1 install.sh: chmod +x in /usr needs doas 2026-07-03 11:17:46 +01:00
joeac 9615c940bb specifies explicit ENVSUBST_VERSION if wget from github 2026-07-03 11:17:17 +01:00
joeac 33e0fb21d6 set -x in install.sh 2026-07-03 11:15:10 +01:00
joeac 2a8b2b3a40 install.sh specifies latest version of envsubst when installing with go
install
2026-07-03 11:11:43 +01:00
joeac f765e1d38f go get -> go install 2026-07-03 11:06:21 +01:00
joeac e6c011d007 removes extraneous " in install.sh 2026-07-03 11:04:37 +01:00
joeac ba8aaa4ade removes redundant README stuff 2026-07-03 11:04:00 +01:00
joeac 9a93c6f983 install.sh installs dependencies 2026-07-03 11:03:36 +01:00
joeac 803e9e00e0 install.sh picks up where left off with env vars 2026-07-03 10:49:19 +01:00
joeac bb795a9555 install.sh picks up with cloning where left off 2026-07-03 10:46:58 +01:00
joeac ac81aeed31 install.sh doesn't attempt to re-add joeac.net user if already exists 2026-07-03 10:44:52 +01:00
joeac 6a4d6d88c5 install.sh creates remote_smtp_password 2026-07-03 10:41:08 +01:00
joeac 32ae7e2b38 checks for some dependencies in install.sh 2026-07-03 10:35:44 +01:00
joeac 4e62da3d52 installs etherpad data dir 2026-07-03 10:29:34 +01:00
joeac b19ae63ceb Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-07-03 10:28:22 +01:00
joeac 17c7e2446c pulls out GEMINI_CERTIFICATES_DIR and GEMINI_COMITIUM_DATA_DIR 2026-07-03 10:28:13 +01:00
joeac 1dab2c159a uninstalling module openrc file uses -f flag to avoid errors 2026-07-03 10:17:07 +01:00
joeac 9cabb1053d adds missing dollar in uninstall_module 2026-07-03 10:17:07 +01:00
joeac caa2f2d59c adds needed sudo to nginx module uninstall 2026-07-03 10:17:07 +01:00
joeac 6a23042b74 makes capitalise func POSIX-compatible 2026-07-03 10:17:07 +01:00
joeac 33d13bb455 appends /data to VAULTWARDEN_DATA_DIR 2026-07-03 10:13:09 +01:00
joeac 0074ad6643 pulls out ETHERPAD_DATA_DIR 2026-07-03 10:12:49 +01:00
joeac e2d3c05376 make install installs vaultwarden data dir 2026-07-03 10:09:47 +01:00
joeac c62f30ec32 adds missing space in openrc.mk 2026-07-03 10:06:10 +01:00
joeac 536a108040 install.sh sets up .env and DIGITALOCEAN_TOKEN 2026-07-03 09:57:48 +01:00
joeac c1525eaa60 automatically makes user.USER service for initing XDG_RUNTIME_DIR 2026-07-03 09:30:26 +01:00
joeac dcd2e1cd19 adds install.sh to install joeac.net under user joeac.net 2026-07-03 09:18:18 +01:00
joeac 19865adb6a pulls out USER variable 2026-07-03 09:02:22 +01:00
joeac b705e9e1e5 nginx installation uses templates and vars instead of explicit conf for
each module
2026-07-02 23:16:29 +01:00
joeac 87a6a3f1f8 adds PUBLIC_ROOT_DIR_ln to vars.mk 2026-07-02 22:33:50 +01:00
joeac 5914e2e578 removes capitalise from container.mk as now in vars.mk 2026-07-02 22:28:43 +01:00
joeac e39c7a47fc etherpad/settings is templated for port 2026-07-02 22:28:13 +01:00
joeac 24c55e142b PORT_module is aliased to MODULE_PORT 2026-07-02 22:27:50 +01:00
joeac de19519be0 makes module rules for all modules, not just ones to be installed here 2026-07-02 22:24:07 +01:00
joeac 3d87e928d4 make_module uses Makefile, not install.mk 2026-07-02 22:23:55 +01:00
joeac c89f338601 re-orders vars.mk 2026-07-02 17:55:00 +01:00
joeac 6feaddc542 adds some line breaks to vars.mk 2026-07-02 17:52:25 +01:00
joeac eaecbcf41f moves ports from .env to make 2026-07-02 17:51:33 +01:00
joeac 7b07b75747 Revert "sets MASTER_NODE to blade-canongate, not pi-broughton"
This reverts commit 0f397b0ee3.
2026-07-02 17:06:11 +01:00
joeac 0f397b0ee3 sets MASTER_NODE to blade-canongate, not pi-broughton 2026-07-02 17:03:44 +01:00
joeac 69918e681f microlog post: 2026-07-02 2026-07-02 16:21:33 +01:00
joeac cb90b72836 only pushes if is own image 2026-07-02 09:50:13 +01:00
joeac f5a6ffa8de COMPOSE_CMD specifies IMAGE_PREFIX 2026-07-02 09:48:13 +01:00
joeac d3845601e9 gets container_image_name from podman-compose config 2026-07-02 09:44:20 +01:00
joeac 1385472aaf push_module does not require build_module 2026-07-02 09:42:26 +01:00
joeac 5fd15c706b build_module only requires Dockerfile if one exists 2026-07-02 09:41:07 +01:00
joeac 5bd502c72f better crontab with update.sh 2026-07-02 09:39:10 +01:00
joeac f39c4a51dd adds empty build, push rules 2026-07-02 09:35:47 +01:00
joeac 8e857abf29 Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-07-02 09:34:38 +01:00
joeac 2abd95da6d has empty make_module rules 2026-07-02 09:34:38 +01:00
joeac 15a3697679 removes any existing joeac.net openrc service file before installing 2026-07-02 09:26:33 +01:00
joeac a2d7299e72 tidy Makefile 2026-07-02 09:23:33 +01:00
joeac dcdda64b9e distinguishes NGINX_SUBDOMAINS from SUBDOMAINS 2026-07-02 09:19:09 +01:00
joeac 5cc5b04b18 moves subdomain defs to top of vars.mk so can be used for SUBDOMAINS var 2026-07-02 09:17:14 +01:00
joeac 0c87d0cc65 Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-07-02 09:08:02 +01:00
joeac a7ea5780cc prunes dyndns crontabs on reinstall 2026-07-02 09:06:46 +01:00
joeac 6f06fc9503 prunes nginx sites on reinstall 2026-07-02 09:04:02 +01:00
joeac 6bc1f2d05f prunes nginx sites on reinstall 2026-07-02 08:53:24 +01:00
joeac c736041f3c adds uninstall_nginx_module rule content 2026-07-02 08:29:49 +01:00
joeac 8a5ad04776 adds reinstall rule 2026-07-02 08:10:25 +01:00
joeac 6a726a331d simplifies uninstall rule with var 2026-07-02 08:10:13 +01:00
joeac b0fe5b4a44 uses vars to simplify install rule 2026-07-02 08:05:51 +01:00
joeac 0e4aaabb54 all nginx modules are installed on master node 2026-07-01 18:45:42 +01:00
joeac 8ddbae5ee2 fix foreach in BUILD_RULES, PUSH_RULES 2026-07-01 18:40:13 +01:00
joeac cfa0dd5a9a adds reinstall/uninstall nginx module rules 2026-07-01 18:39:20 +01:00
joeac 5644789d64 all rule builds stuff 2026-07-01 18:37:44 +01:00
joeac c7c8a96a57 moves helper makefiles into make/ dir 2026-07-01 18:31:34 +01:00
joeac f73512107c renames some module rules 2026-07-01 18:26:22 +01:00
joeac f8401d00a4 fixes indent in dyndns.mk 2026-07-01 18:22:53 +01:00
joeac b79037c055 only install openrc module if it is an openrc module 2026-07-01 18:20:01 +01:00
joeac 8af9a55187 move uninstall_joeac.net_service to openrc.mk 2026-07-01 18:19:43 +01:00
joeac 1b200f217b move vars to vars.mk 2026-07-01 18:19:26 +01:00
joeac c0e6d13a1f moves stuff to nginx.mk 2026-07-01 18:11:08 +01:00
joeac 2b00e03999 pulls out uninstall to openrc.mk 2026-07-01 17:55:30 +01:00
joeac bf2dabe3f4 pulls out more dyndns stuff to dyndns.mk 2026-07-01 17:49:04 +01:00
joeac d0f549ce22 removes unneeded nginx vars in Makefiles 2026-07-01 17:33:53 +01:00
joeac 89165e9d88 pulls vars out to container.mk 2026-07-01 17:29:16 +01:00
joeac a601e93d00 pulls out nginx_module.mk 2026-07-01 17:22:50 +01:00
joeac fa4d275cc0 pulls out container.mk 2026-07-01 17:11:59 +01:00
joeac 9867d4f232 pulls out openrc.mk 2026-07-01 16:51:44 +01:00
joeac e3b3484905 switches to directory for submake file 2026-07-01 16:47:06 +01:00
joeac def393b930 only restart openrc service if one exists on reinstall 2026-07-01 16:47:06 +01:00
Joe Carstairs c1096e2b01 pulls out nginx.mk 2026-07-01 15:46:05 +01:00
Joe Carstairs 04e1dad952 pulls out crontab.mk 2026-07-01 15:24:01 +01:00
Joe Carstairs e170bafffc pulls out dyndns.mk 2026-07-01 15:21:01 +01:00
Joe Carstairs 23f31a8c4d Makefile installs modules per hostname 2026-07-01 15:10:23 +01:00
joeac d5a5375379 add missing " in ln.joeac.net/milehouse 2026-06-30 17:06:31 +01:00
joeac 7d520d864e Makefile: adds reinstall rule 2026-06-30 16:59:23 +01:00
joeac 16063402be post changes update index in http 2026-06-30 16:41:00 +01:00
joeac 0520592fc5 fix typo: errant ) in Makefile 2026-06-30 16:23:26 +01:00
joeac f27a25fdf5 Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-06-30 16:20:24 +01:00
joeac 0181a703f9 add microlog post 2026-06-30 2026-06-30 16:20:20 +01:00
joeac aad7967402 fix ln.joeac.net/milehouse 2026-06-30 13:47:35 +01:00
joeac 7814c0323e remove empty vaultwarden dir from tracking 2026-06-30 10:16:22 +01:00
joeac eb1ee09769 fixes meta http-equiv tags for redirects 2026-06-30 10:02:28 +01:00
joeac 5265f3e83c uninstall uses sub-install.mk 2026-06-30 09:49:34 +01:00
joeac 5aa03f88dc fixes ln/install.mk 2026-06-30 09:46:17 +01:00
joeac ff2a56ac12 improves yq command 2026-06-30 09:43:50 +01:00
joeac 51ce3374b9 adds ln module 2026-06-30 09:36:12 +01:00
joeac 16576fd1f6 can have a sub-install.mk 2026-06-30 09:35:32 +01:00
joeac 3ff52465dd Makefile doesn't assume that every service has Makefile and Compose
service
2026-06-30 09:35:18 +01:00
joeac 0e9f8e2dd6 silences which complaining if no arch 2026-06-30 08:51:23 +01:00
joeac 4225f3c8da Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-06-30 08:23:19 +01:00
joeac fff8c26df9 more container prefixes in compose.yml 2026-06-29 19:38:17 +01:00
joeac 7d8a986784 adds sane defaults in example.env 2026-06-29 19:29:38 +01:00
joeac 4e25b796ca remove bollocks advice on LOCAL_SMTP_USER 2026-06-29 19:27:30 +01:00
joeac 581c1c0c0a see also LOCAL_SMTP_PORT in example.env 2026-06-29 19:26:46 +01:00
joeac 8d1b6b114a uses env vars in compose.yml 2026-06-29 19:24:48 +01:00
joeac 75ca9d485d adds etherpad module 2026-06-29 12:01:49 +01:00
joeac 74dc86d56c fixes /etc/periodic/daily/joeac.net in Makefile 2026-06-29 12:00:17 +01:00
joeac 3e3c866ec0 uninstalls nginx module confs in Makefile 2026-06-29 11:59:47 +01:00
joeac 2b0c63a783 removes errant ) in Makefile 2026-06-29 11:59:15 +01:00
joeac 8386beac1d installs dyndns in Makefile 2026-06-29 11:59:02 +01:00
joeac dc9694fb32 Makefile: realpath ~ instead of realpath ~/.config/.. which may not exist 2026-06-29 09:29:19 +01:00
joeac 70a155a0a7 Makefile: CPU_ARCH can use arch instead of lscpu to find CPU architecture 2026-06-29 09:28:53 +01:00
joeac 673aaf1c13 installs nginx per module 2026-06-29 09:06:17 +01:00
joeac c059bc34d8 adds pwd.joeac.net.conf to nginx 2026-06-29 07:40:09 +01:00
joeac f2a3552286 abuses container_name in compose.yml 2026-06-29 07:32:35 +01:00
joeac f491fe517a adds vaultwarden module on pwd.joeac.net 2026-06-29 07:32:35 +01:00
joeac f6c8f4fae6 remove redundant uninstall_service rule in Makefile 2026-06-29 07:32:35 +01:00
joeac b43ea19e1e starts joeac.net services on boot 2026-06-29 07:32:35 +01:00
joeac c6ff40e288 moves openrc -> openrc/init.d 2026-06-29 07:32:35 +01:00
joeac 86509e2598 installs joeac.net services as separate OpenRC services 2026-06-29 07:32:35 +01:00
joeac adc5498641 adds nginx config, can make install_nginx 2026-06-29 07:32:29 +01:00
joeac 74d779ee35 adds nginx config, can make install_nginx 2026-06-26 10:40:50 +01:00
joeac cc25a91cf1 adds advisory openrc/user-runlevel-default 2026-06-26 09:45:19 +01:00
joeac 13c909f085 chmod 700 in http.Dockerfile (WTF?) 2026-06-25 19:10:25 +01:00
joeac 2823433f7c Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-06-25 19:04:23 +01:00
joeac abfb28dea2 http.Dockerfile: db dir is owned by nginx and mode 600 2026-06-25 19:04:12 +01:00
joeac ebac73704b rename 2026-06-25.md -> .gmi 2026-06-25 18:57:54 +01:00
joeac 74f1df6893 provides SMTP_PORT, SMTP_USER and SMTP_PASSWORD explicitly in compose.yml ;( 2026-06-25 18:54:13 +01:00
joeac 1ef4c22ec6 apk add php85-pdo, php83-pdo_sqlite in http.Dockerfile 2026-06-25 18:52:59 +01:00
joeac 262516d3da apk add gettext gnutls in smtp.Dockerfile (deps of msmtp) 2026-06-25 18:52:02 +01:00
joeac 1304c5180f Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-06-25 17:52:13 +01:00
joeac c3cabed6e7 db is g+rw in http.Dockerfile 2026-06-25 17:51:59 +01:00
Joe Carstairs 274b79f293 microlog post 2026-06-25 2026-06-25 16:29:45 +01:00
joeac aa76b5ad79 prepares DB_PATH in http.Dockerfile 2026-06-25 12:48:47 +01:00
joeac 84496271c4 adds http/vendor to http image 2026-06-25 12:48:07 +01:00
joeac ac4670492e fixes COPY instructions in gemini.Dockerfile 2026-06-25 11:56:33 +01:00
joeac aaed0c2547 adds gcc (dependency for agate) in gemini/Dockerfile 2026-06-25 11:47:54 +01:00
joeac ed13289934 compose down timeout is 5s, not 300s 2026-06-25 11:45:36 +01:00
joeac a575ecdd68 don't make anything in gemini/run.sh 2026-06-25 11:30:55 +01:00
joeac 0a2a4c68fa rc-update adds joeac.net service to 'default' 2026-06-25 11:29:58 +01:00
joeac d9692da89c replaces 'stop' with 'down' to remove containers (so that they get
re-created with fresh images)
2026-06-25 11:29:12 +01:00
joeac fbbc8d2d8b Revert "adds --rm to podman-compose up command to remove containers after"
This reverts commit 812d59e4dd.
2026-06-25 11:27:33 +01:00
joeac d27186a177 copies gemini/run.sh into image 2026-06-25 11:26:22 +01:00
joeac 812d59e4dd adds --rm to podman-compose up command to remove containers after
stopped
2026-06-25 11:25:10 +01:00
joeac 995c258aa5 adds install, uninstall rules to Makefile 2026-06-25 11:17:01 +01:00
joeac bd9af70a97 removes dependencies from openrc/joeac.net 2026-06-25 11:17:01 +01:00
joeac 465c40b927 removes extra command make from openrc/joeac.net 2026-06-25 11:17:01 +01:00
joeac 6f11b8c453 removes redundant JOEACNET_USER var from openrc/joeac.net 2026-06-25 11:17:01 +01:00
joeac 86ec9e9d54 uses rootless podman in Makefile 2026-06-25 11:17:01 +01:00
joeac 78fe3e7956 bin/pp is executable and a dependency of out rule in http/Makefile 2026-06-25 11:17:01 +01:00
joeac ad8a8c3cac openrc/joeac.net 2026-06-25 11:17:01 +01:00
joeac 46596fc5f6 maps env vars for msmtp image in smtp.Dockerfile 2026-06-25 11:15:54 +01:00
joeac f09bbf8d4a replaces prune_blog script with http/Makefile rules 2026-06-25 09:49:14 +01:00
joeac 83e0f1cda5 http: replaces prune_out script with make rules 2026-06-25 08:22:27 +01:00
joeac aa4f63a8ce uses grep short flags for POSIX compatibility 2026-06-24 22:43:32 +01:00
joeac 499a10837f minor changes to composer.lock 2026-06-24 22:12:49 +01:00
joeac 405ccd1709 adds composer install to http/Makefile 2026-06-23 09:00:49 +01:00
joeac b735c95c84 php-fpm gets env vars 2026-06-23 08:43:40 +01:00
joeac e61a0f1de9 php-fpm logs errors 2026-06-23 08:43:35 +01:00
joeac 52b013e165 fastcgi SCRIPT_FILENAME looks in /usr/share/nginx/html 2026-06-23 08:27:52 +01:00
joeac 76081fd88f adds =404 to end of try_files directive 2026-06-23 08:18:46 +01:00
joeac 4a9649f096 adds ; in fastcgi_pass directive in http/nginx.conf 2026-06-23 08:14:40 +01:00
joeac 061b6986a3 php-fpm.conf lives in /etc/php85/fpm/pool.d in http image 2026-06-23 07:58:32 +01:00
joeac ff49052baa php-fpm runs on socket in /run 2026-06-23 07:57:33 +01:00
joeac 7bd5255c63 http.Dockerfile: runs php-fpm and nginx 2026-06-23 00:06:54 +01:00
joeac 473f4b19f0 nginx.conf: moves server into http block 2026-06-23 00:06:44 +01:00
joeac e04d1e55a1 adds nginx/php-fpm config 2026-06-22 23:45:46 +01:00
joeac e23f053b89 http ports: 8080:80, not 8080:8080 2026-06-22 22:59:37 +01:00
joeac ddc416a45a Makefile: build_$(module) depends on $(module).Dockerfile 2026-06-22 22:56:20 +01:00
joeac c07f55e8d4 move smtp/Dockerfile to smtp.Dockerfile 2026-06-22 22:55:34 +01:00
joeac d3e832f064 http/Dockerfile: COPY to nginx folder 2026-06-22 22:53:53 +01:00
joeac 6e5c1c158c gemini/Makefile: FEED_TARGET includes comitium.json, not
subscriptions.json
2026-06-22 22:47:14 +01:00
joeac 07a326114e cleans $(FEED_TARGET) before rebuildings comitium feeds in
gemini/Makefile
2026-06-22 22:46:18 +01:00
joeac 2725d4085c removes line breaks from gemini.Dockerfile for prettiness 2026-06-22 22:42:12 +01:00
joeac 860cd61ba5 adds placeholder rules to smtp/Makefile 2026-06-22 22:41:57 +01:00
joeac 4bfd6d05ca gemini.Dockerfile: don't make in image build, make before image build 2026-06-22 22:34:52 +01:00
joeac 9e0af5fdff gemini.Dockerfile: apk add make 2026-06-22 22:26:27 +01:00
joeac 8ac3e35b1a crond version has -r30 2026-06-22 22:23:31 +01:00
joeac cc4d918a07 Revert "forget about crond container"
This reverts commit 63e3b7fb17.
2026-06-22 19:17:05 +01:00
joeac 63e3b7fb17 forget about crond container 2026-06-22 19:09:14 +01:00
joeac 562cfaebf8 removes unnecessary http args from compose.yml 2026-06-22 19:02:02 +01:00
joeac b47b670d94 uses crond container in gemini.Dockerfile 2026-06-22 19:01:46 +01:00
joeac 7aa8403b70 adds stuff for http to example.env 2026-06-22 19:01:21 +01:00
joeac 6ec142e8f4 replace another :: with cut for POSIX compatibility 2026-06-22 17:51:14 +01:00
joeac 49cf91b789 rss.trivial.upphtml, not .uppxml 2026-06-22 17:50:06 +01:00
joeac 4437e86f72 replace 'source' with '.' in rss.uppxml for POSIX compatibility 2026-06-22 17:37:18 +01:00
joeac 8ec5e261d4 http/Makefile: can cp pp.1 manpage to share 2026-06-22 17:33:52 +01:00
joeac 815c820e6a gitignores bin/pp, share/man/man1/pp.1 2026-06-22 17:27:31 +01:00
joeac 4490194389 replaces :: with cut for POSIX compatibility in upp* source files 2026-06-22 17:26:29 +01:00
joeac 71052b8c26 pp.1 symlink uses relative path 2026-06-22 17:12:10 +01:00
joeac dc43029892 removes POSIX-incompatible :: substitutions in prune_blog 2026-06-22 14:05:35 +01:00
joeac bf592223de forget symlink: cp pp 2026-06-22 13:53:46 +01:00
joeac 8b3fb3f4a2 don't track pp.o 2026-06-22 13:46:12 +01:00
joeac e9909ef057 build_$(module) depends on make_$(module) 2026-06-22 13:44:05 +01:00
joeac e49ccc5aa3 no more prefixes in container_image_name (incompatible with compose.yml) 2026-06-22 13:42:35 +01:00
joeac e78c1cba6d container_image_name includes joeac.net- prefix 2026-06-22 13:37:52 +01:00
joeac 689ff9bd5c podman-compose builds to $(container_image_name) 2026-06-22 13:37:44 +01:00
joeac 7c3ce01b27 podman uses sudo 2026-06-22 13:31:52 +01:00
joeac 6081b7ad79 http.Dockerfile copies build output instead of making it 2026-06-22 13:29:52 +01:00
joeac 0e902c1f57 Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2026-06-22 13:29:22 +01:00
joeac df20a2ea1e smtp/Dockerfile uses msmtp package on git.joeac.net 2026-06-22 13:16:28 +01:00
joeac 870fdc3f65 adds root Makefile 2026-06-22 09:24:36 +01:00
joeac 5f11d27f58 adds smtp/Makefile (empty) 2026-06-22 09:12:35 +01:00
joeac e9df237ca9 adds prune to all rule in http/Makefile 2026-06-22 08:58:20 +01:00
joeac 8b6081dbbf http.Dockerfile uses docker.io/arm32v7/nginx as base 2026-06-19 15:38:02 +01:00
joeac f064c794e3 reorder gemini.Dockerfile 2026-06-19 13:56:23 +01:00
joeac 960e959bd6 get agate image from repository 2026-06-18 21:27:34 +01:00
joeac ba30ad22f8 get comitium image from repo 2026-06-18 21:26:23 +01:00
joeac 29db3ce86a first draft of http.Dockerfile 2026-06-18 20:16:22 +01:00
joeac 2d89475f0b renames capsule -> gemini 2026-06-18 20:01:32 +01:00
joeac f71fbdafac updates readme: swaps website for http 2026-06-18 19:58:41 +01:00
joeac 90b16dad40 remove website service from compose.yml 2026-06-18 19:36:13 +01:00
joeac 05b9308c1b adds redirect from links/rss.xml -> microlog/rss.xml 2026-06-18 19:22:18 +01:00
joeac ca8e6824ea remove Astro website 2026-06-18 19:21:59 +01:00
joeac a0f2505728 updates subscribe.upphtml: no /rss.xml 2026-06-18 19:21:01 +01:00
joeac 3651679c86 adds microlog/rss.uppxml 2026-06-18 19:18:01 +01:00
joeac b0bffe5d72 rss.xml resilient in case multiple blog posts on the same day 2026-06-18 19:15:56 +01:00
joeac 86446de9a8 meta.upphtml accepts NO_META flag 2026-06-18 19:11:02 +01:00
joeac a63b8c6a0b replaces <hr /> with --- in 2024-07-16.gmi 2026-06-18 19:09:42 +01:00
joeac ef681069c7 adds component_files to blog_post_rule, and adds component_files
variable
2026-06-18 19:08:42 +01:00
joeac 5797bc85bf adds blog_rss_feed_rule to http/Makefile 2026-06-18 19:08:17 +01:00
joeac 4c829133a3 adds component files to out_rule deps 2026-06-18 19:07:56 +01:00
joeac 2328446520 more sophisticated escape_upphtml in gmi2upphtml 2026-06-18 19:07:45 +01:00
joeac af4adc0fc8 remove random ; in rss.uppxml 2026-06-18 19:04:34 +01:00
joeac f61a00d292 rss.uppxml uses ' instead of " for version 2026-06-18 19:04:23 +01:00
joeac 7d4ce6f073 rss.xml uses description instead of content:encoded 2026-06-18 19:04:11 +01:00
joeac db5b029f78 blog/rss.xml has no meta tags in content 2026-06-18 19:03:39 +01:00
joeac e8754afca2 blog/rss.xml escapes HTML 2026-06-18 19:03:24 +01:00
joeac c179adccb2 replace <hr> with <hr/> 2026-06-18 18:25:28 +01:00
joeac caed7bbfdf http: adds blog/rss/xml 2026-06-18 10:13:54 +01:00
joeac ff9df11a15 symlinks http/share/man/man1/pp.1 -> http/pp/pp.1 2026-06-18 09:49:51 +01:00
joeac 5396812290 blog/microlog posts have link to index 2026-06-18 09:47:46 +01:00
joeac 9996a47d0c fixes missing post titles in microlog 2026-06-18 09:43:52 +01:00
joeac abd249030b http: adds post.css to blog/microlog posts 2026-06-18 09:43:32 +01:00
joeac 3c132ca4dc http/Makefile doesn't prune non-post files in
http/src/blog,http/src/microlog
2026-06-18 09:25:04 +01:00
joeac f219908d67 prune_blog takes optional BLOG_POST_PATTERN argument 2026-06-18 09:22:49 +01:00
joeac 5e371cf734 prune_blog also removes argfiles 2026-06-18 09:20:50 +01:00
joeac 76901304c4 Makefile clean doesn't clean non-post files under blog,microlog 2026-06-18 09:18:03 +01:00
joeac 83c25e328f only gitignores blog/microlog posts, not everything under
http/src/blog,http/src/microlog
2026-06-18 08:58:32 +01:00
joeac ccd7140d6d http: redirects blog/subscribe to /subscribe 2026-06-18 08:58:02 +01:00
joeac 0db52b89f0 adds trivial html layout 2026-06-18 08:57:48 +01:00
joeac 93cda87bbd http: adds microlog index 2026-06-18 08:46:45 +01:00
joeac 35ef330df8 fix css bug in http microlog: ul inside microlog posts not getting list
markers
2026-06-18 08:45:51 +01:00
joeac 348b76aa04 http/Makefile: fix bug, only upphtml files being built because no
layouts exist for other formats
2026-06-18 08:44:29 +01:00
joeac df748183ab http: adds /subscribe 2026-06-18 08:19:45 +01:00
joeac e470dfc258 http/Makefile includes layout files in deps 2026-06-18 08:17:08 +01:00
joeac 56e2c037b5 moves <main> to default.layout 2026-06-18 08:16:51 +01:00
joeac c5c4167361 remove 2025-10-05 blog post 2026-06-17 22:08:18 +01:00
joeac 2f70f87f41 reverses blog post order 2026-06-17 22:06:05 +01:00
joeac 0519d155fa fixes blockquote block margins 2026-06-17 22:04:36 +01:00
joeac 6d801d0f0e symlinks http/bin/pp to http/pp/pp 2026-06-17 21:54:36 +01:00
joeac fd21d77526 adds pp build to http/Makefile 2026-06-17 21:54:16 +01:00
joeac 5b927900ae increases pp max buffer size 64,000 chars 2026-06-17 21:49:02 +01:00
joeac 5fcb2bcc83 gitignores pp build output 2026-06-17 21:48:24 +01:00
joeac 6dd4caf947 adds pp sources 2026-06-17 21:48:24 +01:00
joeac 506d1adfcc fix bug in Makefile out_rule 2026-06-17 18:35:39 +01:00
joeac e506f7c2e6 reorganises Makefile, adds headings 2026-06-17 18:31:46 +01:00
joeac 574b2d6646 Makefile: named params in out_rule macro 2026-06-17 18:27:13 +01:00
joeac b0adbd4419 parameterises blogs in all rule 2026-06-17 18:23:27 +01:00
joeac 8125081177 parameterises blogs in clean rule 2026-06-17 18:23:11 +01:00
joeac 229339dc17 macroises prune_blog rules 2026-06-17 18:22:43 +01:00
joeac 99b54df287 fix bug in prune_blog: rm '' 2026-06-17 18:21:28 +01:00
joeac 70689047ca macroises blog_rule 2026-06-17 18:19:09 +01:00
joeac b4be3ed2a6 re-orders Makefile and renames some vars 2026-06-17 18:18:51 +01:00
joeac 8e814917b6 Makefile: blogs are configurable 2026-06-17 17:59:18 +01:00
joeac a8686d9169 re-orders Makefile 2026-06-17 09:56:48 +01:00
joeac 830f63641b factors out prune_blog to bin 2026-06-17 09:56:48 +01:00
joeac 3e0def4305 remove unnecessary continue indices in prune_out 2026-06-17 09:34:05 +01:00
joeac 722a304023 factors out prune_out to bin 2026-06-17 09:23:42 +01:00
joeac 423fa1ed26 Makefile: macroises blog_rule 2026-06-17 09:12:09 +01:00
joeac 6cdc0ad1e6 Makefile: renames blog_rule macro 2026-06-17 09:10:37 +01:00
joeac 65b3169d98 fix bug in blog script: set layout_name to default 2026-06-17 09:08:39 +01:00
joeac 122febceb4 public rule follows symlinks 2026-06-16 22:09:31 +01:00
joeac a3cc84b617 separates prune from all 2026-06-16 22:07:58 +01:00
joeac 20855b2976 http/Makefile: updates all rule with new incremental rules 2026-06-16 22:04:10 +01:00
joeac 93d9d86361 adds prune_out rule 2026-06-16 22:03:54 +01:00
joeac f83df38d5e out rule is incremental 2026-06-16 22:03:47 +01:00
joeac dcd563fdc7 silences prune rules 2026-06-16 22:03:14 +01:00
joeac 84ea47f08d copying public happens in Makefile, not mkws 2026-06-16 22:02:46 +01:00
joeac b79a0873dd can pass PATH_PATTERN to mkws 2026-06-16 21:56:01 +01:00
joeac 9adbe55164 move sitemap.uppxml into src 2026-06-16 21:55:33 +01:00
joeac 248b3088b3 rename rules to prune 2026-06-16 21:23:38 +01:00
joeac f653f7ba23 http/Makefile: more iterative 'all' rule 2026-06-15 18:18:58 +01:00
joeac be6466c0ff fix blog script bug: multiple posts on the same day overwrite each other 2026-06-15 18:16:26 +01:00
joeac c2aef5c998 http/Makefile: pattern rule for iterative blog building 2026-06-15 18:13:07 +01:00
joeac 3094407195 blog script now takes optional -f FILE_PATTERN arg 2026-06-15 18:12:46 +01:00
joeac 00af1b4b56 http/Makefile: can remove deleted posts 2026-06-15 18:09:59 +01:00
joeac e10f74c5b6 http: adds blog index page 2026-06-12 10:16:09 +01:00
joeac e2b8cdbce3 http: default layout passes SITE_URL to pages 2026-06-12 10:15:56 +01:00
joeac 6cd664dba6 remove unnecessary backslash from grep expression in http/bin/blog 2026-06-12 10:12:25 +01:00
joeac 0d8f27357a rename SRC -> PAGE in blog.layout.upphtml to avoid overwriting SRC 2026-06-12 09:32:59 +01:00
joeac ce1ebb3ad3 extracts longlog metadata including updated date 2026-06-09 21:29:10 +01:00
joeac 0065d36bc8 blog script takes extract_args_cmd 2026-06-09 21:17:05 +01:00
joeac 7a952de4f8 chmods I guess? 2026-06-09 21:16:35 +01:00
joeac 37d61b5b21 fixes passing POST_TITLE to postprocessor from blog script 2026-06-09 21:03:16 +01:00
joeac 75f5a8bbf5 fixes passing POST_TITLE as an arg to mkws 2026-06-09 21:02:50 +01:00
joeac da1670c57c fixes links in gmi2upphtml 2026-06-09 19:46:34 +01:00
joeac 87dec12b01 cut longlog metadata from blog 2026-06-09 18:20:23 +01:00
joeac 547db8ac94 2024-01-14 longlog post: 'Published:' -> 'Published on:' 2026-06-09 18:19:42 +01:00
joeac 0892937941 parameterises postprocess cmd in blog script 2026-06-09 18:19:16 +01:00
joeac b4f9be57d3 http: builds blogs from gemlogs 2026-06-09 16:11:06 +01:00
joeac 3ecf19cb8a moves <main> from layout to individual pages 2026-06-09 09:14:18 +01:00
joeac 531c9d5a38 swaps " for ' in error.upphtml 2026-06-09 09:12:41 +01:00
joeac 2d6d17261c http: adds contact page 2026-06-04 15:16:34 +01:00
joeac 668c105ccf http: adds otp-dialog component 2026-06-04 15:16:34 +01:00
joeac 813836fbba http: adds scripts 2026-06-04 15:16:34 +01:00
joeac fac1199cd6 http: adds actions.js file to interface JS -> PHP 2026-06-04 15:16:34 +01:00
joeac 171c957c40 http: adds php api routes under do/ 2026-06-04 15:16:34 +01:00
joeac 231ffbd0e8 adds mail.php 2026-06-04 15:14:03 +01:00
joeac e4cd434586 adds db.php 2026-06-04 15:12:26 +01:00
joeac 38370c101f http: adds phpmailer dependency 2026-06-04 15:11:10 +01:00
joeac a89e757718 http: inits php composer 2026-06-04 15:10:45 +01:00
joeac e58cbf7b72 http: adds error page 2026-06-02 18:30:12 +01:00
joeac 902ad835b4 http: fixes meta tags 2026-06-02 18:27:20 +01:00
joeac 3f4df7c503 updates index.upphtml to match existing homepage 2026-06-02 18:12:44 +01:00
joeac a304b1f744 changes " to ' in uppcss files to work around bug in pp 2026-06-02 18:12:29 +01:00
joeac 94e5dfd091 http: add navbar to l.upphtml 2026-06-02 18:07:43 +01:00
joeac bdd64372fd adds images symlink to http/public 2026-06-02 18:02:14 +01:00
joeac ec2cd858bd not sure what this change is, maybe a chmod on mkws? 2026-06-02 18:00:37 +01:00
joeac 6bc4552d41 mkws copies from public if it exists 2026-06-02 18:00:14 +01:00
joeac 86f40be2cb mkws outputs to 'out' instead of 'public' 2026-06-02 17:59:45 +01:00
joeac 7ac7ed3cec feed.uppcss: changes " to ' to work around pp bug 2026-06-02 17:50:16 +01:00
joeac a1866254f9 sitemap.uppxml: uses find instead of wildcard expansion to find html files inside
subfolders
2026-06-02 17:45:39 +01:00
joeac b7fc80b5f5 http/Makefile: public target depends on bin 2026-06-02 17:35:19 +01:00
joeac 629dd5525e http/Makefile: all cleans before building 2026-06-02 17:35:04 +01:00
joeac 0897d11405 microlog: post 2026-06-02 2026-06-02 15:05:55 +01:00
joeac 8725d22874 http/Makefile 2026-05-12 21:40:09 +01:00
joeac b386c66837 delete weird file that got created by accident 2026-05-12 21:30:10 +01:00
joeac 3759fd137c mkws processes css 2026-05-12 21:29:38 +01:00
joeac eb48cdb9a3 inits http ssg with mkws 2026-05-12 20:56:32 +01:00
joeac 512f130117 Fix error log in capsule/build_microlog.bash 2026-05-08 20:41:32 +01:00
joeac 0ca370c285 fix capsule Makefile 2026-05-08 20:40:52 +01:00
joeac 0092231f3d microlog post: 2026-05-08.1 2026-05-08 19:43:43 +01:00
joeac edfc6e77a2 feeds: Radical with Amol Rajan 2026-05-08 19:26:55 +01:00
212 changed files with 4753 additions and 15530 deletions
+3
View File
@@ -5,3 +5,6 @@ indent_size = 2
[*.{md,markdown,mdx}]
max_line_length = 80
[{Makefile,*.mk}]
indent_style = tab
+1
View File
@@ -26,3 +26,4 @@ pnpm-debug.log*
**/*.tfstate.backup
*.sqlite
DIGITALOCEAN_TOKEN
+66
View File
@@ -0,0 +1,66 @@
include make/vars.mk
submake_file = $(shell [ -f "$(module)/Makefile" ] && echo "$(module)/Makefile")
install_submake_file = $(shell [ -f "$(module)/install.mk" ] && echo "$(module)/install.mk")
define make_module_rule =
.PHONY: make_$(module)
make_$(module): $(module)/.env
$(if $(submake_file),\
$(MAKE) --makefile=$(notdir $(submake_file)) --directory=$(module))
endef
define module_env_rule =
$(module)/.env: .env
cp .env $(module)/.env
endef
define install_module_rule =
.PHONY: install_$(module)
install_$(module): install_openrc_$(module) install_dyndns_$(module) $(if $(SUBDOMAIN_$(module)),install_tls_$(SUBDOMAIN_$(module))) $(install_submake_file)
$(if $(install_submake_file),$(MAKE) --makefile=$(notdir $(install_submake_file)) --directory=$(dir $(install_submake_file)) install)
endef
define reinstall_module_rule =
.PHONY: reinstall_$(module)
reinstall_$(module): reinstall_openrc_$(module) reinstall_dyndns_$(module) $(if $(SUBDOMAIN_$(module)),reinstall_tls_$(SUBDOMAIN_$(module))) $(install_submake_file)
$(if $(install_submake_file),$(MAKE) --makefile=$(notdir $(install_submake_file)) --directory=$(dir $(install_submake_file)) reinstall)
endef
define uninstall_module_rule =
.PHONY: uninstall_$(module)
uninstall_$(module): uninstall_openrc_$(module) uninstall_dyndns_$(module) $(if $(SUBDOMAIN_$(module)),install_tls_$(SUBDOMAIN_$(module)))
$(if $(install_submake_file),\
$(MAKE) --makefile=$(notdir $(install_submake_file)) --directory $(dir $(install_submake_file)) uninstall
)
endef
.PHONY: all
all: $(ENV_RULES) $(MAKE_RULES) $(BUILD_RULES) $(PUSH_RULES)
$(foreach module,$(ALL_MODULES),$(eval $(call make_module_rule)))
$(foreach module,$(ALL_MODULES),$(eval $(call module_env_rule)))
.PHONY: install
install: install_tls install_nginx $(ENV_RULES) $(INSTALL_RULES) install_crontab
.PHONY: reinstall
reinstall: reinstall_tls reinstall_nginx reinstall_dyndns $(ENV_RULES) $(REINSTALL_RULES) reinstall_crontab
.PHONY: uninstall
uninstall: uninstall_nginx uninstall_dyndns uninstall_tls uninstall_joeac.net_service $(UNINSTALL_RULES) uninstall_crontab
$(foreach module,$(ALL_MODULES),$(eval $(install_module_rule)))
$(foreach module,$(ALL_MODULES),$(eval $(reinstall_module_rule)))
$(foreach module,$(ALL_MODULES),$(eval $(uninstall_module_rule)))
.PHONY: clean
clean:
$(foreach module,$(MAKE_MODULES),$(MAKE) --directory=$(module) clean;)
include make/container.mk
include make/openrc.mk
include make/nginx.mk
include make/crontab.mk
include make/dyndns.mk
include make/tls.mk
+86 -27
View File
@@ -2,44 +2,103 @@
Joe Carstairs' public Internet presence
Structure:
To install:
```
/
├── capsule My Gemini capsule
├── smtp A local SMTP server
└── website My Website
```sh
wget -O- https://git.joeac.net/joeac/joeac.net/raw/branch/main/install.sh | sh
```
## Running with Podman
## DNS setup
These instructions will probably work with Docker, too: just substitute `podman`
for `docker` in all the commands.
A/AAAA DNS records are added automatically by `make install`, but in order to
get `mox` working as an email server, you'll have to manually add the following
DNS records.
To run with Podman, first set up your environment variables. Copy `example.env`
to `.env` and edit the values accordingly.
Deliver mail for mail.joeac.net to mail.joeac.net:
Then, create the `remote_smtp_password` secret, storing the password for the
remote SMTP server which will send the contact emails on behalf of the website.
```bash
sudo podman secret create remote_smtp_password /path/to/remote/smtp/password
```
mail.joeac.net. MX 10 mail.joeac.net.
```
Now build and start the containers:
All emails from the email server will be signed with two DKIM keys. So that
clients can verify the authenticity of the messages, provide these keys as TXT
records:
```bash
sudo podman-compose build && sudo podman-compose up -d
```
2026a._domainkey.mail.joeac.net. TXT v=DKIM1;h=sha256;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvYfjrzmYgBtYofzBwI80aiW98+M2g6z+gd1Iwz9g0y30rPFNTctFn9GwBNuYBgZiZxB+sqjEbPMbr3li/R7i0A9t/KbNNJhkNC+4IjKJjk+jw1CXm4vXOUa4YSYWwy7NVYTH/QwZGz6fwjVM7YvDnnE4gG2NwrVx+AXlONt2R1G+qgV6HAIIvVi8T0yCjjqEc5B5bKlqk0XU9vSyUFJhhKnR/KNRe79C+H9GWJzcU7HUCmIHX04Xi0JeB/wm3weF1xjtGTsWyy5BmHHsfWGqSr2Dbg5o6AI5W0h4VkQ4QzdEYGVQ9ZBDyqFQwQFXLn0oHZjCD/vFzPOPdM5pxF/OgwIDAQAB
2026b._domainkey.mail.joeac.net. TXT v=DKIM1;h=sha256;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcrk9FUt6AdrvnAP3KawuOTLw7uL5SJ+ZYmShuz41zwM6bPQteGSSwddFXIxcqVlJrdFahrK4KvHX/sw/hWVfZoPLDdwsGN5eI8cqQjNDE+JDu9BbPlTituva4Hkve0hbAKDqA8jmbcZg6aU7b44Kzq8UpWAlPO273Rq2tsbCBcITt8B3NFoeY9CSsZU1LqGl855GUtaNyhlPaAvfab3Q9/4wyusPhCHlBYaRK+ZzuSMs5KEOG6n4kbZfMVi2+4c/bPU5PdTuyvbSIEqjNH4TpfatE0I9ubGv0WbAzr5EZbv5+xtukZ/dIisPPMjn1AbjpSJYNYr2OYgey6+WvzRmQIDAQAB
```
## Running on the host machine
Specify the MX host is allowed to send for our domain and for itself (for DSNs).
~all means softfail for anything else, which is done instead of -all to prevent
older mail servers from rejecting the message because they never get to looking
for a dkim/dmarc pass.
To run on the host machine, first, as before, set up your environment variables
by copying `example.env` to `.env` and editing the values as appropriate.
```bash
npm run start
```
mail.joeac.net. TXT v=spf1 ip4:217.155.190.42 ip6:fdc9:6aec:7a18:0:2e0:4cff:fe61:9b17 mx ~all
```
Note that emails may not work locally without further setup. These instructions
are of course woefully incomplete.
Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)
should be rejected, and request reports. If you email through mailing lists that
strip DKIM-Signature headers and don't rewrite the From header, you may want to
set the policy to p=none.
```
_dmarc.mail.joeac.net. TXT v=DMARC1;p=reject;rua=mailto:dmarcreports@mail.joeac.net!10m
```
Remote servers can use MTA-STS to verify our TLS certificate with the WebPKI
pool of CA's (certificate authorities) when delivering over SMTP with
STARTTLSTLS.
```
mta-sts.mail.joeac.net. CNAME mail.joeac.net.
_mta-sts.mail.joeac.net. TXT v=STSv1; id=20260705T153220
```
Request reporting about TLS failures.
```
_smtp._tls.mail.joeac.net. TXT v=TLSRPTv1; rua=mailto:tlsreports@mail.joeac.net
```
Client settings will reference a subdomain of the hosted domain, making it
easier to migrate to a different server in the future by not requiring settings
in all clients to be updated.
```
clientsettings.mail.joeac.net. CNAME mail.joeac.net.
```
Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by
Microsoft.
```
autoconfig.mail.joeac.net. CNAME mail.joeac.net.
_autodiscover._tcp.mail.joeac.net. SRV 0 1 443 mail.joeac.net.
```
For secure IMAP and submission autoconfig, point to mail host.
```
_imaps._tcp.mail.joeac.net. SRV 0 1 993 mail.joeac.net.
_submissions._tcp.mail.joeac.net. SRV 0 1 465 mail.joeac.net.
```
Next records specify POP3 and non-TLS ports are not to be used. These are
optional and safe to leave out (e.g. if you have to click a lot in a DNS admin
web interface).
```
_imap._tcp.mail.joeac.net. SRV 0 0 0 .
_submission._tcp.mail.joeac.net. SRV 0 0 0 .
_pop3._tcp.mail.joeac.net. SRV 0 0 0 .
_pop3s._tcp.mail.joeac.net. SRV 0 0 0 .
```
Optional: You could mark Let's Encrypt as the only Certificate Authority allowed
to sign TLS certificates for your domain.
```
mail.joeac.net. CAA 0 issue "letsencrypt.org"
```
-38
View File
@@ -1,38 +0,0 @@
FROM alpine:3.23 AS agate
RUN apk --update --no-cache add rust cargo \
&& wget -O - https://github.com/mbrubeck/agate/archive/refs/tags/v3.3.20.tar.gz | tar -xz \
&& cargo install --path agate-3.3.20/ \
&& apk del rust cargo \
&& rm -rf agate-3.3.20 agate-target
FROM alpine:3.23 AS comitium
RUN apk --no-cache add make go scdoc
RUN wget -O - https://git.sr.ht/~nytpu/comitium/archive/v1.8.2.tar.gz | tar -xz \
&& make --directory=comitium-v1.8.2 \
&& mv comitium-v1.8.2/build/comitium /usr/local/bin/comitium \
&& apk del make go scdoc \
&& rm -rf comitium-v1.8.2
FROM alpine:3.23 AS final
RUN mkdir -p /var/app/content
WORKDIR /var/app
COPY capsule/.certificates .certificates
RUN crontab -l > crontab.tmp \
&& echo "0 */6 * * * /usr/local/bin/comitium refresh --data /var/app/comitium-data" >> crontab.tmp \
&& crontab crontab.tmp \
&& rm crontab.tmp
# bash is needed to run the build scripts
# busybox-openrc provides rc-service, which runs crond
# gcc is a dependency for agate
# make is needed to run the Makefile
RUN apk --no-cache add bash busybox-openrc gcc make
RUN rc-update add crond
COPY --from=agate /root/.cargo/bin/agate /usr/local/bin/agate
COPY --from=comitium /usr/local/bin/comitium /usr/local/bin/comitium
COPY capsule .
COPY common /var/common
RUN make
CMD ./run.sh
-24
View File
@@ -1,24 +0,0 @@
COMITIUM_DATA := comitium-data
TEMPLATES_DIR := templates
MICROLOG_HEADER := $(TEMPLATES_DIR)/microlog-header.gmi
LONGLOG_HEADER := $(TEMPLATES_DIR)/longlog-header.gmi
LOGLOG_HEADER := $(TEMPLATES_DIR)/loglog-header.gmi
MICROLOG_ENTRIES := $(wildcard microlog/*)
LONGLOG_ENTRIES := $(wildcard content/logs/longlog/*)
all: microlog loglog longlog add_feeds
microlog: $(MICROLOG_ENTRIES) $(MICROLOG_HEADER)
bash ./build_microlog.bash
longlog: $(LONGLOG_ENTRIES) $(LONGLOG_HEADER)
bash ./build_longlog.bash
loglog: microlog longlog $(LOGLOG_HEADER)
bash ./build_loglog.bash
add_feeds: $(COMITIUM_DATA)/feeds.txt
rm -f $(COMITIUM_DATA)/comitium.json
while read feed; do \
comitium add --data "$(COMITIUM_DATA)" $$feed; \
done <$(COMITIUM_DATA)/feeds.txt
+1 -1
View File
@@ -1,6 +1,6 @@
# Hararis Sapiens on Religion
Published: 14 Jan 2024
Published on: 14 Jan 2024
Ive been slowly re-reading Yuval Noah Hararis 2014 classic, Sapiens, which apart from being ridiculously over-scoped and hilariously under-evidenced, is proving delightfully entertaining.
+1 -1
View File
@@ -167,7 +167,7 @@ Some authors have managed to recognise the ambiguity of the term. Both King et a
So it seems that although many academics have treated the term hallucination as jargon, in actual fact, there is no widely agreed specific meaning of the word.
<hr>
---
I will slide in here with a quick side note. While some authors treated hallucination as jargon for unfaithfulness or unfactuality, other authors contemporaneously managed to talk about these topics without using the word hallucination at all. Here are some examples:
-146
View File
@@ -1,146 +0,0 @@
# "381: how the church as we know it was made"
The church which defines our world now is in a significant way the one which emerged out of sixty years of controversy from the Council of Constantinople in 381. I've been charting what happened, why, and the ongoing legacy.
Published on: 5 Oct 2025
Athanasius defined the fourth century. Not that he was a god, or even a king, or that he always got his way. But he wrote the history books. His tale of an epic battle fought tooth-and-nail between Arian heretics and him and his loyal allies has come to be the standard account of how, over the course of the fourth century, the Church redefined what orthodoxy means and how it is declared and identified.
The result was the Nicene Creed. It had been first written for a very particular polemical purpose in 325, but later found itself the centre of a strange theological revival, and was finally revised in a council at Constantinople in 381. In so doing, the bishops assembled a recognisable 'Nicene' tradition which is still one of the defining features of planet Earth.
For better and for worse, the church as we know it has a capacity both for great humility, faith and submission to the mystery of God, but it also has a capacity for great intolerance. This is the church created in 381.
To understand the church as we know it today, then, we need to understand the complex, confusing journey from 325 to 381.
Athanasius' chronicle of that journey is temptingly simple. The only problem with it is that it isn't true. Indeed, his 'history' was never meant to function as an all-encompassing narrative of Church history, to be read for centuries ever after. His accounts function as polemics, meant to cajole, condemn and persuade his readers in his own time of his vision for their future.
Nevertheless, whatever Athanasius' real significance in how his times unfolded, his witness is important. He fully inhabited his times, often in the middle of the fray. Whether or not we buy Athanasius' portrayal of himself as fighting the good fight, he was certainly a fighter. By looking through his eyes, then, we can get a perspective on how the Church as we know it came to be.
So it makes sense to start with him. As a young priest in his native Alexandria, he became tangled up in a controversy which would come to define his career. A strong-minded and fearless young priest had begun to preach. His name was Arius.
---
According to the Egyptian tradition, Alexander, the bishop of Alexandria, was the nineteenth in a direct line of succession from Mark the Evangelist himself. With a great deal of justice, he would have regarded himself as one of the most important Christian leaders in the world, and at least the equal of the bishop of Rome.
Small wonder, then, that the insubordination that plagued his diocese bothered him. First Erescentius had started a schism, disputing the rule he used for calculating the date of Easter.
Then there was Meletius. During the persecution under the Roman emperor Diocletian, Meletius had already rubbed a few people the wrong way: while other bishops were in hiding or in prison, he took the initiative to resolve problems and ordain priests without properly consulting the absent bishops' representatives. Perhaps it was intended kindly: it was seen as meddling. Now Meletius accused Alexander of being too soft on Christians who had caved into the threat of torture and made sacrifices to the pagan cults. When he decided Alexander was never going to match his high rigorist standards, he broke away, too.
Alexander must have longed for the relatively good order of the Greek and Roman churches, where bickering subordinates were generally willing to let their bishop have the last say. The throne of St Mark was in trouble. If Christ's body wasn't to get chopped up any more than it already was, he needed to establish his personal authority.
This was the context in which Arius, a young firebrand priest, steps onto stage right. He surely knew his own bishop's teaching: God is one substance and one essence, unchangeable, indivisible. Christ his Son is in every way God: God from God, light from light, true God from true God, eternally begotten of the Father before all ages. How else could Christ, by adopting human flesh, mediate the transcendent God to fallen humanity?
But Arius didn't like this one bit. If God is unchangeable, how could he adopt flesh? That suggests he was not flesh, and then became flesh. And in any case, if the martyrs were right to give up their lives to know God, he must have the perfect, uncompromising transcendence which the martyrs so admired. But how can God adopt flesh, never mind suffer and die on a cross, without compromising that transcendence? Something had to give. For Arius, the solution was to modify the relationship between the Father and the Son.
Arius accepted that Christ had to be in some sense divine, in order to mediate God to humanity. But he denied that he was quite as much God as God is. He has something like his Father's essence, not in a co-equal way, but rather in a derivative way. This makes sense of Father-Son language, which suggests the Father came first, and the Son came next, a derivative of the Father. So the Son is God from God, but not true God from true God. The Son was begotten in time, and is not eternal: only God the Father himself is eternal.
At another time in another place, Arius might have passed for a creative, independent thinker without much notice. But Arius was directly contradicting Alexander just as the latter was desperate to assert his authority. It got ugly.
Alexander called a council of local bishops in about 320. The council condemned Arius and removed him from his post as priest. In response, Arius went on the campaign trail, visiting bishops in Palestine and Asia Minor who he thought would be sympathetic to his theology. Shortly afterwards, he returned to Alexandria, triumphantly brandishing vindications from two councils, one in Jerusalem and one in Bithynia. He wasn't going to make it easy for Alexander.
Luckily for Alexander, the Emperor Constantine had just united the eastern and western halves of the Empire. He had famously converted to Christianity after seeing the sign of the cross at the Battle of Milvian Bridge in 312, and saw the bishops as means towards his mission of uniting the Empire under one government and one God. Constantine had been made aware of the dispute between Arius and Alexander, and he didn't want schisms in the church any more than Alexander did.
He called a council in his own palace in Nicaea, paying the travel expenses and hotel bills of all the bishops in attendance. For those bishops, many carrying the scars of torture they had endured under Diocletian, it must have been a bewildering experience. Alexander was in attendance. His secretary was Athanasius.
In 325, the council condemned Arius. To avoid anyone else following in his path, they produced a statement of faith, designed to exclude Arius' teaching, no matter who taught it. This statement of faith is now known as the Nicene Creed.
The council also fixed the date of Easter to boot. Alexander must have been relieved.
You might have thought that would have been the end for Arius. In fact, Constantine engineered his re-admittance into the church as soon as 328. Arius died in peace in 336. Constantine's mission wasn't to purge the church, but to unite the church. As long as all sides worshipped God and could live in peace, he wanted as many people as possible included. His mission was unity, not uniformity.
Bishops like Eusebius of Caesarea in Syria got this. He had been provisionally excommunicated on suspicion of Arianism in 325, but was reconciled at Nicaea given the chance to explain himself and sign up to the Nicene Creed. No sooner had he done this, however, than he had started explaining to the faithful back home how they could carry on believing that the Son was not really eternal, even as the Creed was designed to exclude exactly such a claim. While Eusebius might seem duplicitous, at the time, this was exactly the kind of tolerant pragmatism that Constantine asked of the bishops: as long as they didn't cause more out-and-out conflict.
Alexander didn't have long to enjoy the peace of Nicaea. He died just a few years afterward in 328. The throne of St Mark passed to Athanasius.
---
The peace didn't last long. Just as Athanasius was donning his mitre, Eusebius was plotting against Eustathius the bishop of Antioch, and engineered his deposition. In his defence, Eusebius accused Eustathius of the long-condemned heresy, Sabellianism. Then in 335, he followed up by deposing Marcellus, the bishop of Ancyra, at a council in Tyre.
To defend his action, he wrote _Against Marcellus_, in which he accused Marcellus of being a Sabellian, too. Sabellius' heresy was (to borrow a modern term) modalism, the view that 'Father', 'Son' and 'Spirit' are mere titles, aspects, 'modes' of God, not in any real way distinct. He also accused Marcellus of adoptionism, another agreed heresy. Marcellus taught that the Son only became an aspect of the divine nature at the Incarnation, and that in the last day, Christ would hand over his kingdom to his Father.
This action would cast a long shadow over the next half-century. Time and again, bishops allied to Eusebius' way of thinking, or 'Eusebians', would re-affirm their opposition to that 'heretic' Marcellus and his 'Sabellianism'. This is a crucial dynamic for understanding where theological factions drew up their battle lines, and for what compromises were needed in order to get to 381.
Even the bishop of Alexandria wasn't immune from Eusebius' purge. Athanasius had vigorously defended his ally, Marcellus, at the council of Tyre in 335. Eusebius set about plotting his downfall. He dug up dirt. He accused Athanasius of using threats and bribes to get himself elected, and sending goons to beat up his political opponents. Once he'd found evidence of Athanasius meddling with the crucial Egyptian grain export that kept Rome fed, he had the emperor on side. Constantine convened a meeting in 336 and exiled him to the German frontier.
---
Or at least, that's how Athanasius tells it. Athanasius loves a plot: at the time, alleging a conspiracy was a classic rhetorical technique for painting your enemies as heretics.
Eusebius was no stranger to rhetoric himself, and it's to his 337 best-seller, the _Life of Constantine_, that we owe our standard account of Constantine's reign. He regarded Empire and Church as allies in a joint mission, to unite the world under one government and one faith. To him, someone like Athanasius, constitutionally incapable of tolerating anyone who disagreed with him and willing to use gangster tactics to get his way, was a threat to this divine mission.
It's worth remembering that after Constantine died, Athanasius would be re-exiled by four more Roman emperors. In his lifetime, only Julian failed to exile Athanasius, and him only perhaps because he didn't have time in his whirlwind twenty-month reign. We also can't be sure how much influence Eusebius actually had in the expulsion of Athanasius and his allies: it coheres well enough with the emperor's anti-sectarian agenda that it might have happened with or without Eusebius' involvement.
Perhaps Athanasius was a brute. Still, the Roman Catholic Church manages to venerate both Eusebius and Athanasius as saints. This may seem like a contradiction. But perhaps an ability to tolerate contradiction is precisely the legacy of 381.
But we're not there yet. By 335, Eusebius had engineered the exile of Eustathius, Marcellus, and Athanasius. After Constantine died, he had to do it all over again, but by 339, he had persuaded his successor, Constantius, to re-assert his father's exiles of the three men. With the Empire once again split, Athanasius and Marcellus headed to Rome to re-group and re-think.
---
From Rome, Athanasius and Marcellus were safe for now from Eusebius' clutches, but also relatively impotent. In this period of exile in the 340s, in an effort to claw back his reputation, Athanasius developed the polemic which still defines the standard history of the fourth century. He invented a cunning label for Eusebius and his cronies: he called them 'Arians'.
Eusebius rejected the label as ridiculous. Arius had been reconciled, and more to the point, had died in 336. For that matter, why would a bishop follow the teaching of a mere priest? Not only that, but the label ignored significant differences between Arius' and Eusebius' teaching. His verdict was clear: the label 'Arian' is a baseless slur, with no other purpose than to tar his reputation as a heretic.
He was right, of course. But like it or not, Athanasius' theory of an Arian conspiracy began to win adherents, not least Julian, the bishop of Rome. Julian called a council to exonerate Athanasius and Marcellus. When the Greeks refused to turn up, he called a local council anyway and vindicated the two men. In the face of Greek obstinacy, Julian wrote east, pleading the bishops to take the 'Arian' threat seriously.
In response, the easterners held a council in Antioch in 341, agreeing four creeds which powerfully condemned Marcellus' teaching, including the influential Dedication Creed. This includes assertions that Father, Son and Spirit are 'three in subsistence, one in agreement', that the Son was generated before time began, against Marcellus' teaching that the Father, Son and Spirit are aspects of God without division in subsistence, and that there only came to be a divine Son at his incarnation. They explicitly condemned Arius, Sabellius and Marcellus.
So the divisions grew deeper. Without an emperor to compel the bishops to come together, there may not have been much chance of a rapprochement. But even if there were to be such an emperor, who's to say that their settlement would have satisfied the bishops?
---
Meanwhile, in the 340s and through the 350s, two further theological movements gathered steam: the homoians and the heterousians.
The homoians, perhaps tired of the squabbles between the Athanasian and Eusebian factions, determined to sidestep their petty debates altogether.
A key term of the theological disagreement was 'essence' or 'ousia'. Athanasius, in his lifelong battle to make sure Arius stayed dead, insisted that Father, Son and Spirit shared the same ousia. In contrast, Eusebius, with his anti-Sabellian polemic, needed to assert the real distinction between Father, Son and Spirit, and so asserted that each had a separate ousia. So the difference can be summed up as a counting problem. How many divine ousias are there? One or three?
The homoians claimed that both sides were mistaken, simply because they used the word 'ousia'. There is no mention of ousia in Scripture, so, they claimed, we have no basis for asserting it of God one way or the other. All we can truly say is that Father, Son and Spirit are distinct but somehow alike. Whereof we cannot speak, there must we remain silent.
This might have worked as a way forward, except that the heterousians provoked such a strong reaction that 'ousia'-talk was needed to refute them. Aetius, and his followed Eunomius, argued that since God is simple, and all generate things are divided, it follows that God is ingenerate. But the Son is generate: therefore Father and Son must be altogether unalike. They expressed this by saying that Father and Son are unlike in ousia. This teaching was swiftly branded 'neo-Arian', provoking a strong reaction. To counter the heterousian teaching, their opponents were forced to fight on their terms, and that meant using 'ousia'-talk.
Thus enters Basil of Caesarea. He argued that if we abandon 'ousia'-talk, we will have no way of saying that the Father and Son have anything in common at all, which makes a nonsense of the idea that the Son brings humanity knowledge of his Father. Without like essence, they might as well be two completely different Gods. Therefore we have to say at least that they have like essence -- 'homoiousia'. But without direct access to perfect knowledge of the invisible God, we're not in a position to judge that they have exactly the same essence, so he stopped short of agreeing with the 'homoousia' of the Nicene Creed which Athanasius so treasured.
Seeing the opportunity to make common cause against the homoians, Athanasius started to soften. He wrote an extremely charitable commentary on Basil's theology which emphasised their similarities and papered over their differences. Athanasius recognised that both he and Basil wanted to assert the unity of God while still preserving distinctions between Father, Son and Spirit. The two began to campaign against the homoian movement.
But Basil got there too late. In 359, the emperor Constantine II called a council in Constantinople, and in 360 it issued a homoian creed with full imperial backing. Any campaign against the homoians would have to take place sub rosa.
---
In Athanasius' and Basil's long, slow campaign against homoianism, their weapon of choice was surprising: they dusted off the Nicene Creed of 325. Athanasius argued, against the homoians, that 'ousia'-talk, although not directly Scriptural, was essential in order to draw out the consequences of Scripture while ruling out Arian mis-interpretations.
Thus Nicaea, conceived as a one-off meant to clean up the Arian controversy, found a new life as the anti-homoian movement -- or perhaps you could call it the Nicene revival? -- rallied around it.
As the movement progressed, the formerly disagreeing bishops found ways to come together. An essential move was that made in Athanasius' _Antiochene Tome_ of 362. In it, he relented on his long opposition to there being three 'hypostases' or 'substances' in the Godhead.
'Hypostasis' had for a long time been used interchangeably with 'ousia'. However, Athanasius claimed that perhaps God could have three hypostases, but only one ousia, at the same time. In so doing, he wedged apart a sharp technical distinction between 'hypostasis' and 'ousia' which previously wouldn't have made sense. Logical or not, it enabled the Nicene revival to have its cake and eat it. God is both one in ousia, protecting against Arianism, and three in hypostasis, protecting against Sabellianism.
So the Nicene revival gained a new superpower: the power to use formerly synonymous terms to assert contradictions without blushing. This power to accept apparent contradiction as part of the unknowable mystery of God is perhaps the most important legacy of the period. Arguably, the church has been at its best when it has put aside the need to know everything, and embraced this spirit of tolerance, humility and faith.
---
For much of the 360s and 370s, the homoian emperor Valens had ruled over the eastern part of the Empire, while his big brother, Valentinian, ruled the west. In the late 370s, Valentinian and then Valens died within quick succession of each other. Valentinian's twenty-year-old son, Gratian, was left to clear up the mess. In 379, Gratian delegated rule of the east to Theodosius, who was to implement a decisively different religious policy than his predecessor, Valens.
In 380, Theodosius issued an edict, saying that only those who agreed to the homoousios clause of the Nicene Creed could be considered 'catholic' Christians. The message was clear: the homoians were out, and the Nicenes were in.
In 381, he called a council to Constantinople, and it (probably) issued the revision of the 325 creed which is still used in various versions in all the world's largest Christian denominations. There would be no more revisions, and it would become, then as now, compulsory reading for all those preparing to don vestments.
One question is, why did the 381 creed differ in the ways it did from 325? Many of the differences, including the much-enlarged section on the Son, seem to have little controversial content: nobody was disputing that Jesus was born of the Virgin Mary, for example, though she makes her first appearance in the Creed in the 381 version. Some historians think this suggests that the 381 was based on a similar, but distinct creed from 325. This seems unlikely to me, given that about half the creed is in verbatim agreement with 325.
However, a couple of edits stand out. There are some clear signs of anti-Marcellianism: 'his \[the Son's] kingdom shall have no end', the Son is begotten of the Father 'before all ages'. Perhaps a clear emphasis on the eternal relationship between the Son and the Father was part of the diplomacy needed to get the Eusebian faction on-side.
The new details on the Holy Spirit are interesting too. They suggest a delicate compromise. Some bishops were reluctant to suppose that the Father and the Spirit have the same essence. On the other hand, others reckoned that they must share the same essence, given that they are equally deserving of worship. Thus the creed does not have a 'homoousios' clause for the Spirit, but does assert that the Spirit 'together with the Father and with the Son is worshipped and glorified'. With a spoonful of humility, both sides can be satisfied with that.
The revised Nicene Creed was the focus point, the distillation of a growing theological movement, formed by the various anti-homoian bishops finding a way to keep true to their own convictions while respecting each other's red lines.
As a result of the context of 325, Athanasius' relentless anti-Arian polemic which kept that movement alive, and the 'neo-Arian' heterousian movement, the new Nicene tradition insisted on the full co-equal divinity of Father, Son and Holy Spirit. This doctrine ensures Nicenes can affirm that Christ mediates true knowledge of the transcendent Godhead to humanity: the one who was born of Mary, suffered and died on the cross, was raised from the dead and ascended into heaven was true God from true God, of the same essence as his Father.
To satisfy the Eusebian strain, which defined itself by opposition to Marcellus, the Nicene tradition included a commitment to a robust distinction between Father, Son and Spirit, and to the eternity of the Son: begotten of the Father before all ages, his kingdom shall have no end. As a result, Nicenes inherited a way of thinking about God's action in the world, as instrinsically co-operative without being divided.
The biggest change between 325 and 381 was not the text, but what the text is used for. In 325, the Creed functioned to condemn Arius in order to heal the divisions his teachings had caused. In its second life, the Creed found an altogether new purpose: to serve as a common statement of orthodox faith. It started life as a way to define who was out. It ended up defining who was in.
Where was Athanasius? Consider that when Athanasius was appointed bishop in 328, he was relatively young for a bishop at thirty-five. That means that in 381, he would have been the ripe old age of eighty-eight. In fact, he didn't make it that far: he died in peace in the countryside outside his native Alexandria in 373. If he had seen the outcome of 381, he might have regarded his life project complete. Perhaps he knew that with the new generation of bishops, the tide was turning for good, and died in peace. Perhaps not. Either way, his compromises, and his beloved homoousios, have left a permanent mark on the church.
This is the legacy of 381. It is two-faced: any common statement of faith can be used to exclude. Indeed, in the late fourth century, both non-Nicene Christians and pagans found themselves the victims of increasing state-backed sectarian violence.
However, 381 also bears witness to the power of humility and faith. Once we stop grasping at perfect knowledge we cannot attain, we can begin to appreciate the mystery of God. This is one legacy I hope we can carry forward into our century.
+3
View File
@@ -0,0 +1,3 @@
I loved Climate Change As Class War by Matthew Huber. I realise I'd been hooked into the assumption that the only kinds of green action people outside the corridors of power can take is making green consumption choices, taking up gardening, or going on a protest. This assumption leads to the conclusion that you and I are, ultimately, powerless over the big global problems. I hadn't consider the kind of action Huber considers: industrial action leading to state socialism.
=> https://www.versobooks.com/en-gb/products/775-climate-change-as-class-war Climate Change As Class War by Matthew Huber
+5
View File
@@ -0,0 +1,5 @@
I've just written a new abstract for my upcoming Masters dissertation. Hopefully it shows how my thinking is developing.
(I'm also just really pleased that I managed to write something today. I've been suffering from blank page syndrome.)
Since the 1960s, a political movement has emerged in the West, dominated by the middle classes, seeking to defend the non-human world against human abuse: the green movement. Greens have developed a palette of common rhetorical techniques, which we can call green rhetoric. Since the 1990s, climate change has become the master frame for green issues, and green rhetoric has been dominated by the narrative of an impending climate catastrophe. However, in that time, most of the movements most important demands for social-scale change have not happened: green rhetoric is not working. Some scholars, attempting to explain whats gone wrong, pinpoint how green rhetoric uses apocalyptic language, and implies that green rhetoric would be more effective were it to be less apocalyptic. In this dissertation, I argue against this conclusion, and aim to show that, in order to succeed, green rhetoric must become more apocalyptic, not less.
+7
View File
@@ -0,0 +1,7 @@
I looked through some concordances while I was bored in the New College Library the other day.
I really love Robert Young's Analytical Concordance. Young's is a concordance to the Authorised Version, so the entries are the English words used in the AV. But each English-language entry groups together all the words in the Greek New Testament and Hebrew Old Testament which correspond to that English word, and lists all the occurrences of each of those words in their respective source texts. That makes it a perfect tool for understanding the relationships between the biblical source texts. The only disadvantage is that it doesn't seem at all interested in the Septuagint, which is really important for understanding the way New Testament authors use the Old Testament.
It astonished me that there are concordances available of the NIV and ESV texts. Why would you want that? The texts don't correspond very well to the Greek or Hebrew source texts, and they don't have nearly as much clout as self-standing faith texts as the Authorised Version does. I really don't get it. If I had one, I would probably never open it.
I couldn't get ahold of a Strong's, but I did happen to drop in Armchair Books on the way back from the library, and they have a beautiful 19th-century Cruden's Concordance. Cruden's was the first English-language concordance when its eponymous author painstakingly constructed it in 1737, and has never been out of print since then. By the looks of it, it remains very relevant for English-speaking readers of the Bible. And going for a bargain as well... if you get there before me, I'll eat your head.
+7
View File
@@ -0,0 +1,7 @@
So I've now got half a dozen services running on my homelab across two machines. I need to install the services on each machine, keep them running after crashes and reboots, update and restart them when the source code changes, and co-ordinate them with each other (since I can only have one public IP on my home network, but I want to serve stuff over the Internet from both machines).
I am currently managing this with Podman, some Dockerfiles and Docker-Compose files, Makefiles, and a lot of manual interference. This just about works, and is probably reasonably sensible for the small, low-consequence situation.
Still, it is a faff. I'm wondering: is this kind of what Kubernetes is for? I've never understood. Looking at the documentation, I find it really hard to understand what Kubernetes is for. Maybe it's for situations kind of like this (only typically much bigger)?
Answers on a postcard please.
+11
View File
@@ -0,0 +1,11 @@
Writing is hard! I've just spent 2 days fixing 1 paragraph in my dissertation. The problem was to give the reader good reason to think that the fossil fuel industry exerts a strong influence on the majority epistemology. The end result is a single paragraph with four citations. You'd think that would take half a day, not 2 full days.
I think what made it hard was that I needed to gain understanding. If I just needed a fact or two, it would have been easier: skim a few abstracts, extract a few figures, call it a day. But I didn't know in advance what fossil-think looks like. I couldn't write a paragraph that was almost done but for a few missing statistics. I needed to know the whens, the hows, the whos.
What I ended up doing was collecting a score of browser tabs with relevant articles and ebooks (which was easy enough), then going through each one and reading it: properly reading it, for understanding, not just skimming abstracts.
This worked: after about half a dozen articles, I reviewed my notes, realised I had enough to be getting on with, and wrote up my paragraph. Along the way I gained a (very shallow) understanding of how the fossil fuel industry influences public discourse.
But: it took a heck of a long time for one paragraph.
I really wish I had an experienced researcher looking over my shoulder, popping by a couple of times a day, to offer advice. Maybe they would tell me: sorry, that's just how it is, you need to gain understanding and that takes time. Or maybe they would say: you should really be cutting the corner here, let me show you how. It's one of the many things you cannot get from a monthly check-in with a supervisor.
+3
View File
@@ -0,0 +1,3 @@
I've been following Ink and Switch's blog for a while. They're working creatively with computers. This is a great discussion about LLMs which covers what they are, how they are being abused to de-humanise the world, and how things could be different, concluding in a call for 'punk computing.' Let's have it!
=> https://www.inkandswitch.com/tangents/artificial/ Artificial | Ink and Switch
+77 -23
View File
@@ -1,41 +1,95 @@
services:
capsule:
image: git.joeac.net/joeac/joeac.net-capsule
build:
context: .
dockerfile: capsule.Dockerfile
actualbudget:
image: docker.io/actualbudget/actual-server:latest-alpine
container_name: joeac.net-actualbudget
restart: unless-stopped
volumes:
- ${ACTUALBUDGET_DATA_DIR}:/data/
ports:
- "1965:1965"
- "${ACTUALBUDGET_PORT}:5006"
website:
image: git.joeac.net/joeac/joeac.net-website
gemini:
image: git.joeac.net/joeac/${IMAGE_PREFIX}joeac.net-gemini
container_name: joeac.net-gemini
build:
context: .
dockerfile: website.Dockerfile
args:
LOCAL_SMTP_HOST: smtp
LOCAL_SMTP_PORT: 2500
LOCAL_SMTP_PASSWORD: smtp
env_file:
- .env
dockerfile: gemini.Dockerfile
ports:
- "8000:4321"
- "${GEMINI_PORT}:1965"
volumes:
- ${GEMINI_CERTIFICATES_DIR}:/var/app/.certificates
- ${GEMINI_COMITIUM_DATA_DIR}:/var/app/comitium-data
http:
image: git.joeac.net/joeac/${IMAGE_PREFIX}joeac.net-http
container_name: joeac.net-http
build:
context: .
dockerfile: http.Dockerfile
environment:
MAX_DAILY_EMAILS: ${MAX_DAILY_EMAILS}
CONTACT_MAILBOX: ${CONTACT_MAILBOX}
CONTACT_MAILBOX_NAME: ${CONTACT_MAILBOX_NAME}
LOCAL_SMTP_FROM: ${LOCAL_SMTP_FROM}
LOCAL_SMTP_FROM_NAME: ${LOCAL_SMTP_FROM_NAME}
LOCAL_SMTP_HOST: ${LOCAL_SMTP_HOST}
LOCAL_SMTP_PORT: ${LOCAL_SMTP_PORT}
LOCAL_SMTP_USER: ${LOCAL_SMTP_USER}
LOCAL_SMTP_PASSWORD: ${LOCAL_SMTP_PASSWORD}
DB_SCHEME: ${DB_SCHEME}
DB_PATH: ${DB_PATH}
ports:
- "${HTTP_PORT}:80"
etherpad:
image: git.joeac.net/joeac/${IMAGE_PREFIX}joeac.net-etherpad
container_name: joeac.net-etherpad
build:
context: .
dockerfile: etherpad.Dockerfile
args:
ALPINE_VERSION: ${ALPINE_VERSION}
ETHERPAD_VERSION: ${ETHERPAD_VERSION}
volumes:
- ${ETHERPAD_DATA_DIR}:/var/app/var
environment:
NODE_ENV: ${ETHERPAD_NODE_ENV}
ADMIN_PASSWORD: ${ETHERPAD_ADMIN_PASSWORD}
restart: unless-stopped
ports:
- "${ETHERPAD_PORT}:9001"
smtp:
image: git.joeac.net/joeac/joeac.net-smtp
image: git.joeac.net/joeac/${IMAGE_PREFIX}joeac.net-smtp
container_name: joeac.net-smtp
build:
context: smtp
context: .
dockerfile: smtp.Dockerfile
args:
LOCAL_SMTP_PORT: 2500
env_file:
- .env
SMTP_PORT: ${LOCAL_SMTP_PORT}
environment:
LOCAL_SMTP_PORT: 2500
LOCAL_SMTP_PASSWORD: smtp
LOCAL_SMTP_ENVELOPE_FROM: ${LOCAL_SMTP_ENVELOPE_FROM}
REMOTE_SMTP_HOST: ${REMOTE_SMTP_HOST}
REMOTE_SMTP_PORT: ${REMOTE_SMTP_PORT}
REMOTE_SMTP_USER: ${REMOTE_SMTP_USER}
SMTP_PORT: ${LOCAL_SMTP_PORT}
SMTP_USER: ${LOCAL_SMTP_USER}
SMTP_PASSWORD: ${LOCAL_SMTP_PASSWORD}
REMOTE_SMTP_PASSWORD_FILE: /run/secrets/remote_smtp_password
secrets:
- remote_smtp_password
vaultwarden:
image: docker.io/vaultwarden/server:latest
container_name: joeac.net-vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://pwd.joeac.net"
volumes:
- ${VAULTWARDEN_DATA_DIR}:/data/
ports:
- "${VAULTWARDEN_PORT}:80"
secrets:
remote_smtp_password:
external: true
+11
View File
@@ -0,0 +1,11 @@
ARG ALPINE_VERSION=3.23
FROM docker.io/alpine:${ALPINE_VERSION}
RUN mkdir -p /var/app
WORKDIR /var/app
ARG ETHERPAD_VERSION
RUN apk --no-cache add pnpm \
&& ENV=~/.profile SHELL=sh pnpm setup \
&& source ~/.profile \
&& pnpm install --global tsx
COPY etherpad/etherpad-${ETHERPAD_VERSION} .
CMD pnpx tsx src/node/server.ts
+2
View File
@@ -0,0 +1,2 @@
var
etherpad-*
+27
View File
@@ -0,0 +1,27 @@
ETHERPAD_VERSION := 3.3.2
SRC_DIR := etherpad-$(ETHERPAD_VERSION)
.PHONY: all
all: download_etherpad install_deps
.PHONY: download_etherpad
download_etherpad: $(SRC_DIR)
$(SRC_DIR):
wget -O- https://github.com/ether/etherpad/archive/refs/tags/v3.3.2.tar.gz | tar -xz
.PHONY: install_deps
install_deps: $(SRC_DIR)/node_modules
$(SRC_DIR)/node_modules: $(SRC_DIR) $(SRC_DIR)/settings.json $(SRC_DIR)/package.json $(SRC_DIR)/pnpm-lock.yaml
cd $(SRC_DIR) && pnpm install
$(SRC_DIR)/settings.json: $(SRC_DIR) settings.json.template
ifdef ETHERPAD_PORT
ETHERPAD_PORT=$(ETHERPAD_PORT) envsubst -no-unset -i settings.json.template -o $(SRC_DIR)/settings.json
else
$(error ETHERPAD_PORT is not defined)
endif
clean:
rm -rf etherpad-*/
+13
View File
@@ -0,0 +1,13 @@
.PHONY: install
install: $(ETHERPAD_DATA_DIR)
.PHONY: reinstall
reinstall: $(ETHERPAD_DATA_DIR)
$(ETHERPAD_DATA_DIR):
sudo mkdir -p $(ETHERPAD_DATA_DIR)
ETHERPAD_USER=$(whoami) && sudo chown $$ETHERPAD_USER:$$ETHERPAD_USER $(ETHERPAD_DATA_DIR)
.PHONY: uninstall
uninstall:
$(info Nothing to do)
+134
View File
@@ -0,0 +1,134 @@
{
"title": "joeac's docs",
"showRecentPads": true,
"favicon": null,
"skinName": "colibris",
"skinVariants": "super-light-toolbar super-light-editor light-background",
"ip": "0.0.0.0",
"port": ${ETHERPAD_PORT},
"showSettingsInAdminPage": true,
"enableMetrics": "$${ENABLE_METRICS:true}",
"cleanup": {
"enabled": false,
"keepRevisions": 5
},
"dbType": "sqlite",
"dbSettings": {
"filename": "var/etherpad.sq3"
},
"defaultPadText" : "",
"padOptions": {
"noColors": false,
"showControls": true,
"showChat": true,
"showLineNumbers": true,
"useMonospaceFont": false,
"userName": null,
"userColor": null,
"rtl": false,
"alwaysShowChat": false,
"chatAndUsers": false,
"lang": null
},
"padShortcutEnabled" : {
"altF9": true,
"altC": true,
"cmdShift2": true,
"delete": true,
"return": true,
"esc": true,
"cmdS": true,
"tab": true,
"cmdZ": true,
"cmdY": true,
"cmdI": true,
"cmdB": true,
"cmdU": true,
"cmd5": true,
"cmdShiftL": true,
"cmdShiftN": true,
"cmdShift1": true,
"cmdShiftC": true,
"cmdH": true,
"ctrlHome": true,
"pageUp": true,
"pageDown": true
},
"updateServer": "https://etherpad.org/ep_infos",
"suppressErrorsInPadText": false,
"requireSession": false,
"editOnly": false,
"minify": true,
"maxAge": 21600,
"soffice": null,
"docxExport": true,
"allowUnknownFileEnds": true,
"requireAuthentication": false,
"requireAuthorization": false,
"trustProxy": false,
"cookie": {
"keyRotationInterval": 86400000,
"sameSite": "Lax",
"sessionLifetime": 864000000,
"sessionRefreshInterval": 86400000,
"sessionCleanup": true
},
"disableIPlogging": false,
"automaticReconnectionTimeout": 0,
"scrollWhenFocusLineIsOutOfViewport": {
"percentage": {
"editionAboveViewport": 0,
"editionBelowViewport": 0
},
"duration": 0,
"scrollWhenCaretIsInTheLastLineOfViewport": false,
"percentageToScrollWhenUserPressesArrowUp": 0
},
"users": {
"admin": {
"password": "admin",
"is_admin": true
}
},
"socketTransportProtocols" : ["websocket", "polling"],
"socketIo": {
"maxHttpBufferSize": 1000000
},
"loadTest": false,
"dumpOnUncleanExit": false,
"importExportRateLimiting": {
"windowMs": 90000,
"max": 10
},
"importMaxFileSize": 52428800, // 50 * 1024 * 1024
"authenticationMethod": "$${AUTHENTICATION_METHOD:sso}",
"enableDarkMode": "$${ENABLE_DARK_MODE:true}",
"enablePadWideSettings": "$${ENABLE_PAD_WIDE_SETTINGS:false}",
"commitRateLimiting": {
"duration": 1,
"points": 10
},
"logLayoutType": "colored",
"customLocaleStrings": {},
"enableAdminUITests": false,
"lowerCasePadIds": false,
"sso": {
"issuer": "$${SSO_ISSUER:http://localhost:${ETHERPAD_PORT}}",
"clients": [
{
"client_id": "$${ADMIN_CLIENT:admin_client}",
"client_secret": "$${ADMIN_SECRET:admin}",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"redirect_uris": ["$${ADMIN_REDIRECT:http://localhost:${ETHERPAD_PORT}/admin/}"]
},
{
"client_id": "$${USER_CLIENT:user_client}",
"client_secret": "$${USER_SECRET:user}",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"redirect_uris": ["$${USER_REDIRECT:http://localhost:${ETHERPAD_PORT}/}"]
}
]
}
}
Executable → Regular
+35 -3
View File
@@ -1,11 +1,24 @@
# The hostname to use for the local SMTP server
LOCAL_SMTP_HOST=smtp
# The username for authenticating to the local SMTP server
# Recommended to be the same as REMOTE_SMTP_USER, else the remote may reject it
LOCAL_SMTP_USER=
LOCAL_SMTP_USER=smtp
# The password for authenticating to the local SMTP server
LOCAL_SMTP_PASSWORD=smtp
# The envelope-from used by the local SMTP server
# Recommended to be the same as REMOTE_SMTP_USER, else the remote may reject it
# Theoretically should be "LOCAL_SMTP_FROM_NAME <LOCAL_SMTP_FROM>", but in
# practice this seems to cause bugs, while setting it to LOCAL_SMTP_FROM works
LOCAL_SMTP_ENVELOPE_FROM=
# The from address used by the local SMTP server
# Recommended to be the same as REMOTE_SMTP_USER, else the remote may reject it
LOCAL_SMTP_FROM=
# The from name used by the local SMTP server
LOCAL_SMTP_FROM_NAME=
# The host of the remote SMTP server, e.g. smtp.gmail.com
REMOTE_SMTP_HOST=
@@ -18,3 +31,22 @@ REMOTE_SMTP_USER=
# The email address where contact emails will be sent
CONTACT_MAILBOX=
# The name to associate with the contact mailbox
CONTACT_MAILBOX_NAME=
# When this 24-hour limit is reached, the website will no longer
# send emails to the contact mailbox
MAX_DAILY_EMAILS=
# The scheme part of the URI to the website database
DB_SCHEME=sqlite
# The path part of the URI to the website database
DB_PATH=/var/app/joeac.net-http.sqlite
# The Node.JS environment for the Etherpad instance (production, development)
ETHERPAD_NODE_ENV=production
# The admin password for the Etherpad instance
ETHERPAD_ADMIN_PASSWORD=
+19
View File
@@ -0,0 +1,19 @@
FROM git.joeac.net/joeac/${IMAGE_PREFIX}agate:3.3.20-alpine3.23 AS agate
FROM git.joeac.net/joeac/${IMAGE_PREFIX}comitium:1.8.2-alpine3.23 AS comitium
FROM git.joeac.net/joeac/${IMAGE_PREFIX}crond:1.37.0-r30-alpine3.23 AS final
RUN apk --no-cache add gcc # dependency for agate
COPY --from=agate /root/.cargo/bin/agate /usr/local/bin/agate
COPY --from=comitium /usr/local/bin/comitium /usr/local/bin/comitium
RUN mkdir -p /var/app/content
WORKDIR /var/app
RUN crontab -l > crontab.tmp \
&& echo "0 */6 * * * /usr/local/bin/comitium refresh --data /var/app/comitium-data" >> crontab.tmp \
&& crontab crontab.tmp \
&& rm crontab.tmp
COPY gemini/run.sh /var/app/
COPY gemini/content /var/app/content
COPY common /var/common
CMD ./run.sh
+34
View File
@@ -0,0 +1,34 @@
COMITIUM_DATA := comitium-data
TEMPLATES_DIR := templates
MICROLOG_HEADER := $(TEMPLATES_DIR)/microlog-header.gmi
LONGLOG_HEADER := $(TEMPLATES_DIR)/longlog-header.gmi
LOGLOG_HEADER := $(TEMPLATES_DIR)/loglog-header.gmi
MICROLOG_ENTRIES := $(wildcard microlog/*)
LONGLOG_ENTRIES := $(wildcard content/logs/longlog/*)
LOGLOG_TARGET := content/logs/loglog.gmi
FEED_TARGET := $(COMITIUM_DATA)/comitium.json $(COMITIUM_DATA)/feed.gmi $(COMITIUM_DATA)/subscriptions.gmi
FEED_OUTPUTS := $(FEED_TARGET) $(COMITIUM_DATA)/comitium.json $(COMITIUM_DATA)/feed.gmi $(COMITIUM_DATA)/subscriptions.gmi
MICROLOG_TARGET := content/logs/microlog.gmi
MICROLOG_OUTPUTS := $(MICROLOG_TARGET) $(wildcard content/logs/microlog/*)
LONGLOG_TARGET := content/logs/longlog.gmi
.DELETE_ON_ERROR:
all: $(MICROLOG_TARGET) $(MICROLOG_OUTPUTS) $(LONGLOG_TARGET) $(LOGLOG_TARGET) $(FEED_OUTPUTS)
$(MICROLOG_TARGET): $(wildcard microlog/*) $(MICROLOG_HEADER)
bash ./build_microlog.bash
$(LONGLOG_TARGET): $(LONGLOG_ENTRIES) $(LONGLOG_HEADER)
bash ./build_longlog.bash
$(LOGLOG_TARGET): $(MICROLOG_TARGET) $(LONGLOG_TARGET) $(LOGLOG_HEADER)
bash ./build_loglog.bash
$(FEED_TARGET): $(COMITIUM_DATA)/feeds.txt
rm -f $(FEED_TARGET); \
while read feed; do \
comitium add --data "$(COMITIUM_DATA)" $$feed; \
done <$(COMITIUM_DATA)/feeds.txt
clean:
rm -rf $(MICROLOG_OUTPUTS) $(LONGLOG_TARGET) $(LOGLOG_TARGET) $(FEED_OUTPUTS)
@@ -30,7 +30,7 @@ for ((i=${#posts[@]}-1; i>=0; i--)); do
cat "${posts[i]}" >> "$MICROLOG"
trim_microlog_tail
else
echo "Error: $post does not match format YYYY-MM-DD(.n). Aborting..."
echo "Error: ${posts[i]} does not match format YYYY-MM-DD(.n). Aborting..."
exit 1
fi
done
@@ -13,3 +13,4 @@ https://mwgmac.github.io --watch
https://dieterhelm.co.uk/feed/
https://extinctionrebellion.uk/feed/
https://pluralistic.net/feed/
https://podcasts.files.bbci.co.uk/p0gg4k6r.rss "Radical with Amol Rajan"
+22
View File
@@ -0,0 +1,22 @@
GEMINI_COMITIUM_DATA_FILES := $(addprefix $(GEMINI_COMITIUM_DATA_DIR)/,$(notdir $(wildcard comitium-data/*)))
.PHONY: install
install: $(GEMINI_CERTIFICATES_DIR) $(GEMINI_COMITIUM_DATA_FILES)
.PHONY: reinstall
reinstall: $(GEMINI_CERTIFICATES_DIR) $(GEMINI_COMITIUM_DATA_FILES)
$(GEMINI_CERTIFICATES_DIR):
sudo mkdir -p $(GEMINI_CERTIFICATES_DIR)
$(let gemini_user,$(shell whoami),sudo chown -R $(gemini_user):$(gemini_user) $(GEMINI_CERTIFICATES_DIR))
$(GEMINI_COMITIUM_DATA_DIR)/%: comitium-data/% $(GEMINI_COMITIUM_DATA_DIR)
sudo cp $< $@
$(let gemini_user,$(shell whoami),sudo chown $(gemini_user):$(gemini_user) $@)
$(GEMINI_COMITIUM_DATA_DIR):
sudo mkdir -p $(GEMINI_COMITIUM_DATA_DIR)
.PHONY: uninstall
uninstall:
sudo rm -rf $(GEMINI_COMITIUM_DATA_DIR)
+1 -2
View File
@@ -1,5 +1,4 @@
#!/bin/sh
make && \
( ( sleep 5; comitium refresh --data comitium-data/ ) & ) \
( ( sleep 5; comitium refresh --data comitium-data/ ) & ) \
&& agate --content content/ --addr [::]:1965 --addr 0.0.0.0:1965 --lang en-GB
+13
View File
@@ -0,0 +1,13 @@
FROM docker.io/arm32v7/nginx:1.30.3-alpine3.23
RUN apk --update add php85-fpm php85-pdo php85-pdo_sqlite
COPY http/vendor /usr/share/nginx/vendor
COPY http/php /usr/share/nginx/php
COPY http/out /usr/share/nginx/html
COPY http/php-fpm.conf /etc/php85/php-fpm.d/joeac.net-http_pool.conf
COPY http/nginx.conf /etc/nginx/nginx.conf
CMD mkdir -p "$(dirname "$DB_PATH")" \
&& touch $DB_PATH \
&& chown -R nginx:nginx "$(dirname "$DB_PATH")" \
&& chmod -R 700 "$(dirname "$DB_PATH")" \
&& php-fpm85 \
&& nginx -g "daemon off;"
+8
View File
@@ -0,0 +1,8 @@
out
vendor
src/microlog/*.microlog.upphtml
src/microlog/*.microlog.upphtml.args
src/blog/*.blog.upphtml
src/blog/*.blog.upphtml.args
bin/pp
share/man/man1/pp.1
+150
View File
@@ -0,0 +1,150 @@
include config.mk
#############
# VARIABLES #
#############
src_files := $(filter-out %.args,$(wildcard src/*.upp*)) $(filter-out %.args,$(wildcard src/**/*.upp*))
src_files_ext := $(suffix $(src_files))
out_deps_ext := $(src_files_ext:.upp%=%)
component_files := $(shell find -L share/components -type f)
layout_files := $(wildcard share/layouts/*.layout.upp*)
layouts := $(notdir $(basename $(basename $(layout_files))))
remove_all_layouts = $(call remove_layouts,$(layouts),$(1))
remove_layouts = $(let first rest,$(1),$(call remove_layout,$(first),$(if $(rest),$(call remove_layouts,$(rest),$(2)),$(2))))
remove_layout = $(subst .$(1).upp,.,$(2))
out_deps := $(subst .upp,.,$(call remove_all_layouts,$(addprefix out/,$(src_files:src/%=%))))
out_deps_without_src := $(filter-out $(out_deps),$(shell find -L out -type f))
out_deps += $(out_deps_without_src)
blogs := $(subst blogs/,,$(wildcard blogs/*))
#############
# FUNCTIONS #
#############
blog_post_target = src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml
blog_post_src = blogs/$(blog)/%.$(BLOG_EXT_$(blog)) $$(wildcard bin/*)
blog_src = $(patsubst \
blogs/$(blog)/%.$(BLOG_EXT_$(blog)),\
src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml,\
$(shell find -L "blogs/$(blog)" -type f -and -name *.$(BLOG_EXT_$(blog))))
blog_src_without_post_src = $(filter-out $(blog_src),$(shell find -L "src/$(blog)" -type f -and -name *.$(BLOG_LAYOUT_$(blog)).upphtml))
default_layout_file = $(if \
$(shell [ -f "share/layouts/default.layout.$(ext)" ] && echo 1),\
share/layouts/default.layout.$(ext))
layout_file = $(if $(layout),share/layouts/$(layout).layout.upp$(ext),$(default_layout_file))
define out_rule =
out/%.$(ext): src/%$(if $(layout),.$(layout)).upp$(ext) $(layout_file) $(component_files) bin/pp
./bin/mkws https://joeac.net "$$$$(realpath $$<)"
endef
define blog_rss_feed_rule =
out/$(blog)/rss.xml: src/$(blog)/rss.uppxml $(blog_src) $(wildcard bin/*) bin/pp
./bin/mkws https://joeac.net "$$(realpath src/$(blog)/rss.uppxml)"
endef
define blog_rule =
.PHONY: $(blog)
$(blog): $(blog_src_without_post_src) $(blog_src) out/$(blog).html
endef
define blog_post_rule =
$(blog_post_target): $(blog_post_src) $(let ext,html,$(layout_file) $(component_files))
./bin/blog \
-i $(blog) \
$(if $(BLOG_LAYOUT_$(blog)),-l $(BLOG_LAYOUT_$(blog))) \
$(BLOG_FLAGS_$(blog)) \
-f $$*.$(BLOG_EXT_$(blog))
endef
define blog_index_rule =
out/$(blog).html: src/$(blog).upphtml $(blog_src) $(wildcard bin/*) bin/pp
./bin/mkws https://joeac.net "$$(realpath src/$(blog).upphtml)"
endef
define prune_blog_src_file_rule =
$(let rel_basename,$(patsubst src/$(blog)/%.$(BLOG_LAYOUT_$(blog)).upphtml,%,$(file)),
blogs/$(blog)/$(rel_basename).$(BLOG_EXT_$(blog)):
rm $(file)
)
endef
define prune_blog_out_file_rule =
$(let rel_basename,$(patsubst src/$(blog)/%.$(BLOG_LAYOUT_$(blog)).upphtml,%,$(file)),
$(file):
rm out/$(rel_basename).html
)
endef
define prune_out_file_rule =
$(let orig_ext,$(patsubst .%,%,$(suffix $(file))),
$(let ext,$(patsubst upp%,%,$(orig_ext)),
$(let rel_basename,$(patsubst out/%.$(orig_ext),%,$(file)),
$(let src_file,src/$(rel_basename).upp$(ext),
$(let src_files_with_layouts,$(foreach layout,$(layouts),src/$(rel_basename).$(layout).upp$(ext)),
$(let public_file,public/$(rel_basename).$(orig_ext),
$(src_file) $(src_files_with_layouts) $(public_file):
rm $(file)
))))))
endef
#########
# RULES #
#########
all: $(blogs) public out vendor
$(MAKE) out # if blog posts have been pruned, outfiles must be re-calculated
share/man/man1/pp.1:
cp pp/pp.1 share/man/man1/pp.1
.PHONY: out
out: $(out_deps)
bin/pp:
$(MAKE) --directory=pp && chmod +x pp/pp && cp pp/pp bin/pp
$(foreach ext,$(sort $(out_deps_ext)),\
$(eval $(out_rule)))
$(foreach layout,$(layouts),\
$(foreach ext,$(sort $(out_deps_ext)),\
$(eval $(out_rule))))
$(foreach blog,$(blogs),$(eval $(blog_rule)))
$(foreach blog,$(blogs),$(eval $(blog_index_rule)))
$(foreach blog,$(blogs),\
$(foreach layout,$(layouts),\
$(eval $(call blog_post_rule,$(blog),$(layout)))))
$(foreach blog,$(blogs),\
$(eval $(call blog_post_rule,$(blog),)))
$(foreach blog,$(blogs),$(eval $(blog_rss_feed_rule)))
.PHONY: public
public: $(subst public/,out/,$(shell find -L public/ -type f))
out/%: public/%
mkdir -p $(dir $@) && cp $< $@
$(foreach file,$(out_deps_without_src),$(eval $(prune_out_file_rule)))
$(foreach blog,$(blogs),$(foreach file,$(blog_src_without_post_src),$(eval $(prune_blog_src_file_rule))))
vendor: composer.json composer.lock
composer install
.PHONY: clean
clean:
rm -rf \
vendor \
out \
$(foreach blog,$(blogs), \
$(foreach ext,upphtml upphtml.args,\
src/$(blog)/*$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).$(ext)))
Executable
+110
View File
@@ -0,0 +1,110 @@
#!/bin/sh -e
export PATH="$(dirname $(realpath $0)):$PATH"
export BLOGS="$(realpath blogs)"
export SRC="$(realpath src)"
export LAYOUTS="$(realpath share/layouts)"
usage()
{
>&2 cat << USAGE
usage: blog -i LOG [-p POSTPROCESS_CMD] [-a EXTRACT_ARGS_CMD] [-l LAYOUT] [-t] [-f FILE_PATTERN]
options:
-a EXTRACT_ARGS_CMD
If provided, a shell command to extract arguments to the layout from the
source files. The shell will be provided with a variable, POST_TITLE.
-f FILE
If provided, a pattern for the files to process. Default: *.
-i LOG
The name of the source directory under \$BLOGS where log posts live.
-l LAYOUT
If provided, the layout under \$LAYOUTS to use for the generated
upphtml files. If not provided, uses the default layout.
-p POSTPROCESS_CMD
If provided, a shell command to post-process the source files into
upphtml. The shell will be provided with a variable, POST_TITLE.
-t
If enabled, use filenames as post titles
USAGE
}
extract_args_cmd="head -n 0 | echo \"POST_TITLE=\\\"\${POST_TITLE}\\\"\""
file_pattern="*"
log_name=""
layout_name="default"
postprocess_cmd="echo"
use_filenames_for_titles=0
while getopts "a:f:i:l:p:t" opt
do
case $opt in
a)
extract_args_cmd="$OPTARG"
;;
f)
file_pattern="$OPTARG"
;;
i)
log_name="$OPTARG"
;;
l)
layout_name="$OPTARG"
;;
p)
postprocess_cmd="$OPTARG"
;;
t)
use_filenames_for_titles=1
;;
esac
done
if [ -z "${log_name}" ]
then
usage
exit 1
fi
if ! [ -d "${BLOGS}/${log_name}" ]
then
>&2 echo "there is no directory '${input_log}' under ${BLOGS}. options: $(ls common)"
usage
exit 1
fi
layout="${LAYOUTS}/${layout_name}.layout.upphtml"
if [ "${layout_name}" = "default" ]
then
layout_ext=""
else
layout_ext=".${layout_name}"
fi
if ! [ -f "${layout}" ]
then
echo >&2 "Warning: no layout found at $(basename "${LAYOUTS}")/${layout_name}.layout.upphtml"
fi
mkdir -p "${SRC}/${log_name}"
for entry in $(find -L "${BLOGS}/${log_name}" -type f -name "${file_pattern}")
do
basename="$(basename "$entry")"
name="${basename%\.*}"
dest="${SRC}/${log_name}/${name}${layout_ext}.upphtml"
if [ $use_filenames_for_titles -eq 1 ]
then
export POST_TITLE="$name"
cat "$entry" | eval "${postprocess_cmd}" > "$dest"
else
export POST_TITLE="$(grep -e "^# " -m 1 "$entry" | cut -c3-)"
cat "$entry" | grep -e "^# " -v | eval "$postprocess_cmd" > "$dest"
fi
cat "$entry" | eval "$extract_args_cmd" > "${dest}.args"
echo "Processed ${entry#$(dirname ${BLOGS})/} to ${dest#$(dirname ${SRC})/}"
done
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh -e
meta=""
read_published_on=0
while read line
do
meta="${meta}
${line}"
if [ "$(echo "$line" | cut -c1-14)" = "Published on: " ]
then
read_published_on=1
break
fi
done
if [ "$read_published_on" -eq 0 ]
then
echo "$meta"
exit 0
fi
read line
if [ "$(echo "$line" | cut -c1-14)" = "Updated: " ]
then
meta="${meta}
${line}"
else
echo "$line"
fi
while read line
do
echo "$line"
done
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh -e
POST_TITLE="${POST_TITLE:-}"
DATE_PUBLISHED=""
DATE_UPDATED=""
while read line
do
if [ "$(echo "$line" | cut -c1-2)" = "# " ]
then
POST_TITLE="$(echo "$line" | cut -c3-)"
elif [ "$(echo "$line" | cut -c1-14)" = "Published on: " ]
then
DATE_PUBLISHED="$(date --date="$(echo "$line" | cut -c15-)" +%Y-%m-%d)"
elif [ "$(echo "$line" | cut -c1-12)" = "Updated on: " ]
then
DATE_UPDATED="$(date --date="$(echo "$line" | cut -c13-)" +%Y-%m-%d)"
fi
done
if [ -n "$POST_TITLE" ]
then
echo "POST_TITLE=\"${POST_TITLE}\""
fi
if [ -n "$DATE_PUBLISHED" ]
then
echo "DATE_PUBLISHED=\"${DATE_PUBLISHED}\""
fi
if [ -n "$DATE_UPDATED" ]
then
echo "DATE_UPDATED=\"${DATE_UPDATED}\""
fi
+184
View File
@@ -0,0 +1,184 @@
#!/bin/sh -e
post_title="${POST_TITLE:-}"
is_in_list=0
is_in_link_block=0
is_in_preformatted_block=0
is_in_blockquote=0
if_in_list_then_close()
{
if [ $is_in_list -ne 0 ]
then
echo "</ul>"
is_in_list=0
fi
}
if_in_link_block_then_close()
{
if [ $is_in_link_block -ne 0 ]
then
echo "</p>"
is_in_link_block=0
fi
}
if_in_preformatted_block_then_close()
{
if [ $is_in_preformatted_block -ne 0 ]
then
echo "</pre>"
is_in_preformatted_block=0
fi
}
if_in_blockquote_then_close()
{
if [ $is_in_blockquote -ne 0 ]
then
echo "</blockquote>"
is_in_blockquote=0
fi
}
close_all_open_blocks()
{
if_in_list_then_close
if_in_link_block_then_close
if_in_preformatted_block_then_close
if_in_blockquote_then_close
}
escape_upphtml()
{
echo "$1" \
| sed "s/&/\\&amp;/g" \
| sed "s/</\\&lt;/g" \
| sed "s/>/\\&gt;/g" \
| sed "s/\"/\\&quot;/g" \
| sed "s/'/\\&#x27;/g" \
| sed "s/\`/\\&#x60;/g" \
| sed "s/\\\$/\\\\\$/g"
}
close_all_open_blocks_except()
{
except="$1"
if [ "$except" != "list" ]
then
if_in_list_then_close
fi
if [ "$except" != "link" ]
then
if_in_link_block_then_close
fi
if [ "$except" != "pre" ]
then
if_in_preformatted_block_then_close
fi
if [ "$except" != "blockquote" ]
then
if_in_blockquote_then_close
fi
}
if [ -z "$post_title" ]
then
read first_line
if [ "$(echo "$first_line" | cut -c1-2)" != "# " ]
then
echo >&2 "Error: no [post_title] argument provided and no top-level heading at top of the document."
exit 1
fi
post_title="${first_line### }"
fi
echo "#!
export TITLE=\"${post_title} | joeac's blog\"
export DESCRIPTION=\"${post_title} | joeac's blog\"
pp \"\${SHARE}\"/components/meta.upphtml
#!"
while read line
do
if [ "$(echo $line | sed "s/[[:space:]]//g")" = "" ]
then
continue
fi
if [ "$(echo $line | cut -c1)" = "#" ]
then
title="$(escape_upphtml "${line#*# }")"
heading="${line%%$title}"
heading="${heading%% }"
heading_level="${#heading}"
if [ "$(echo "$line" | cut -c$(( $heading_level + 1 )) )" = " " ]
then
close_all_open_blocks
echo "<h${heading_level}>${title}</h${heading_level}>"
continue
fi
fi
if [ "$(echo "$line" | cut -c1-2)" = "=>" ]
then
close_all_open_blocks_except link
href="$(escape_upphtml "$(echo "$line" | sed "s/^=>[[:space:]]\+//" | sed "s/[[:space:]]\+.*//")")"
title="$(escape_upphtml "${line#=> *${href}}")"
title="${title:-${href}}"
if [ $is_in_link_block -eq 0 ]
then
echo "<p>"
else
echo "<br/>"
fi
is_in_link_block=1
echo "=> <a href='${href}'>${title}</a>"
continue
fi
if [ "$( echo "$line" | cut -c1-2)" = "* " ]
then
close_all_open_blocks_except list
if [ $is_in_list -eq 0 ]
then
echo "<ul>"
fi
is_in_list=1
echo "<li>$(escape_upphtml "$(echo "$line" | cut -c3-)")</li>"
continue
fi
if [ "$(echo "$line" | cut -c1)" = ">" ]
then
close_all_open_blocks_except blockquote
if [ $is_in_blockquote -eq 0 ]
then
echo "<blockquote>"
fi
is_in_blockquote=1
echo "<p>$(escape_upphtml "$(echo "$line" | cut -c2-)")</p>"
continue
fi
if [ "$(echo "$line" | cut -c1-3)" = "\`\`\`" ]
then
close_all_open_blocks_except pre
if [ $is_in_preformatted_block -eq 0 ]
then
echo "<pre>"
else
echo -e "\n"
fi
is_in_preformatted_block=1
echo "$(escape_upphtml "$(echo "$line" | cut -c4-)")"
continue
fi
close_all_open_blocks
echo "<p>$(escape_upphtml "${line}")</p>"
done
close_all_open_blocks
Executable
BIN
View File
Binary file not shown.
Executable
+72
View File
@@ -0,0 +1,72 @@
#!/bin/sh -e
export PATH="$(dirname $(realpath $0)):$PATH"
usage() {
>&2 printf "usage: %s url [pattern]\\n" "$(basename "$0")"
exit 1
}
test $# -lt 1 && usage
export SRC="$(realpath src)"
export PUBLIC="$(realpath public)"
export SHARE="$(realpath share)"
export LAYOUTS="$(realpath share/layouts)"
export DEFAULT_LAYOUT="${LAYOUTS}/default.layout.upphtml"
export OUT="$(realpath out)"
if ! test -f "${SRC}"/index.upphtml
then
>&2 printf "no index.upphtml file found\\n"
exit 1
fi
PATH_PATTERN="${2:-*}"
for t in $(find -L "${SRC}/" -type f -and -wholename "${PATH_PATTERN}" -and -name "*\.upp*" -and -not -name "*\.upp*\.*")
do
basename="$(basename "$t")"
ext="${basename##*upp}"
basename_without_ext="${basename%\.upp${ext}}"
penultimate_segment="${basename_without_ext##*\.}"
if [ "$basename_without_ext" != "$penultimate_segment" ] && [ -f "${LAYOUTS}/${penultimate_segment}.layout.upphtml" ]
then
layout="${LAYOUTS}/${penultimate_segment}.layout.upphtml"
name="${basename%\.${penultimate_segment}\.upp${ext}}"
else
layout="${DEFAULT_LAYOUT}"
name="${basename%\.upp${ext}}"
fi
in_dir="$(dirname "$t")/"
rel_dir="${in_dir#${SRC}/}"
mkdir -p "${OUT}/${rel_dir}/"
args="$( [ -f "${t}.args" ] && cat "${t}.args" || echo "" )"
if [ "$(head -n 1 "${layout}")" = "#!" ]
then
>"${layout}.tmp" echo "
#!
${args}
$(tail -n +2 "${layout}")
"
else
>"${layout}.tmp" echo "
#!
${args}
#!
$(cat "${layout}")
"
fi
if [ "$ext" = "html" ]
then
pp "${layout}.tmp" "${t}" "${1}" > \
"${OUT}/${rel_dir}/${name}.${ext}"
else
pp "${t}" "${1}" > \
"${OUT}/${rel_dir}/${name}.${ext}"
fi
rm "${layout}.tmp"
echo "Written ${OUT}/${rel_dir}${name}.${ext}"
done
+1
View File
@@ -0,0 +1 @@
../../common/longlog/
+1
View File
@@ -0,0 +1 @@
../../common/microlog/
+17
View File
@@ -0,0 +1,17 @@
{
"name": "joeacnet/http",
"require": {
"phpmailer/phpmailer": "^7.1"
},
"autoload": {
"psr-4": {
"JoeacNet\\Http\\": "php/"
}
},
"authors": [
{
"name": "Joe Carstairs",
"email": "me@joeac.net"
}
]
}
+101
View File
@@ -0,0 +1,101 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "5145609ea1f80d6d838d0f9649acd36a",
"packages": [
{
"name": "phpmailer/phpmailer",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^10.0.0@dev",
"squizlabs/php_codesniffer": "^3.13.5",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
},
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
],
"time": "2026-05-18T08:06:14+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}
+7
View File
@@ -0,0 +1,7 @@
BLOG_LAYOUT_blog := blog
BLOG_EXT_blog := gmi
BLOG_FLAGS_blog = -p 'cut_longlog_meta | gmi2upphtml' -a extract_longlog_meta
BLOG_LAYOUT_microlog := microlog
BLOG_EXT_microlog := gmi
BLOG_FLAGS_microlog := -t -p gmi2upphtml
+47
View File
@@ -0,0 +1,47 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
listen [::]:80;
server_name joeac.net;
access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
try_files $uri $uri.html $uri/index.html $uri.php $uri/index.php =404;
}
error_page 404 /error.html;
location ~ \.php$ {
root html;
fastcgi_pass unix:/run/php8.5-fpm-joeac.net-http.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.sqlite {
deny all;
}
}
}
+36
View File
@@ -0,0 +1,36 @@
[joeac.net]
user = nginx
group = nginx
listen = /run/php8.5-fpm-joeac.net-http.sock
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
access.log = /var/log/php85/$pool.access.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{milli}d %{kilo}M %C%%"
catch_workers_output = yes
php_admin_value[error_log] = /var/log/php85/$pool.error.log
php_admin_flag[log_errors] = on
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[allow_url_fopen] = off
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500
clear_env = no
env[MAX_DAILY_EMAILS] = $MAX_DAILY_EMAILS
env[CONTACT_MAILBOX] = $CONTACT_MAILBOX
env[CONTACT_MAILBOX_NAME] = $CONTACT_MAILBOX_NAME
env[LOCAL_SMTP_FROM] = $LOCAL_SMTP_FROM
env[LOCAL_SMTP_FROM_NAME] = $LOCAL_SMTP_FROM_NAME
env[LOCAL_SMTP_HOST] = $LOCAL_SMTP_HOST
env[LOCAL_SMTP_PORT] = $LOCAL_SMTP_PORT
env[LOCAL_SMTP_USER] = $LOCAL_SMTP_USER
env[LOCAL_SMTP_PASSWORD] = $LOCAL_SMTP_PASSWORD
env[DB_SCHEME] = $DB_SCHEME
env[DB_PATH] = $DB_PATH
+60
View File
@@ -0,0 +1,60 @@
<?php namespace JoeacNet\Http\Config;
function maxDailyEmails(): int|null
{
$maxDailyEmails = getenv("MAX_DAILY_EMAILS");
if (!is_numeric($maxDailyEmails)) {
return null;
}
return $maxDailyEmails;
}
function contactMailbox(): string|null
{
return (string) getenv("CONTACT_MAILBOX");
}
function contactMailboxName(): string|null
{
return (string) getenv("CONTACT_MAILBOX_NAME");
}
function localSmtpFrom(): string|null
{
return (string) getenv("LOCAL_SMTP_FROM");
}
function localSmtpFromName(): string|null
{
return (string) getenv("LOCAL_SMTP_FROM_NAME");
}
function localSmtpHost(): string|null
{
return (string) getenv("LOCAL_SMTP_HOST");
}
function localSmtpPort(): string|null
{
return (string) getenv("LOCAL_SMTP_PORT");
}
function localSmtpUser(): string|null
{
return (string) getenv("LOCAL_SMTP_USER");
}
function localSmtpPass(): string|null
{
return (string) getenv("LOCAL_SMTP_PASSWORD");
}
function dbScheme(): string|null
{
return (string) getenv("DB_SCHEME");
}
function dbPath(): string|null
{
return (string) getenv("DB_PATH");
}
+205
View File
@@ -0,0 +1,205 @@
<?php namespace JoeacNet\Http\Db;
use JoeacNet\Http\Config;
use PDO;
require_once __DIR__ . "/config.php";
function connectDb(): PDO
{
$pdo = new PDO(Config\dbScheme() . ":" . Config\dbPath());
$pdo = createTables($pdo);
return $pdo;
}
function createTables(PDO $pdo): PDO
{
$pdo->exec(
<<<SQL
CREATE TABLE IF NOT EXISTS otp (
userId TEXT NOT NULL,
value TEXT NOT NULL,
createdAtUnixSecs INTEGER NOT NULL,
validUntilUnixSecs INTEGER NOT NULL
);
SQL
,
);
$pdo->exec(
<<<SQL
CREATE TABLE IF NOT EXISTS send_email_token (
userId TEXT NOT NULL,
value TEXT NOT NULL,
createdAtUnixSecs INTEGER NOT NULL,
validUntilUnixSecs INTEGER NOT NULL
);
SQL
,
);
$pdo->exec(
<<<SQL
CREATE TABLE IF NOT EXISTS sent_emails (
messageId TEXT NOT NULL,
sentAtUnixSecs INTEGER NOT NULL
);
SQL
,
);
return $pdo;
}
function countEmailsSentLast24Hours(PDO $db): int
{
$twentyFourHoursAgo = time() - 24 * 60 * 60;
return (int) $db
->query(
<<<SQL
SELECT COUNT(*) as count
FROM sent_emails
WHERE sentAtUnixSecs >= $twentyFourHoursAgo;
SQL
,
)
->fetchColumn(0);
}
function insertSendEmailToken(PDO $db, string $userId, string $token)
{
$createdAtUnixSecs = time();
$validUntilUnixSecs = $createdAtUnixSecs + 60;
$db
->prepare(
<<<SQL
INSERT
INTO send_email_token
(userId, value, createdAtUnixSecs, validUntilUnixSecs)
VALUES
(:userId, :token, :createdAtUnixSecs, :validUntilUnixSecs);
SQL
,
)
->execute([
"userId" => $userId,
"token" => $token,
"createdAtUnixSecs" => $createdAtUnixSecs,
"validUntilUnixSecs" => $validUntilUnixSecs,
]);
}
function isTokenValid(PDO $db, string $userId, string $token): int
{
$now = time();
$stmt = $db->prepare(
<<<SQL
SELECT COUNT(*) AS count
FROM send_email_token
WHERE userId = :userId
AND value = :token
AND validUntilUnixSecs >= :validUntilUnixSecs;
SQL
,
);
$stmt->execute([
"userId" => $userId,
"token" => $token,
"validUntilUnixSecs" => $now,
]);
$validTokenCount = (int) $stmt->fetchColumn(0);
return $validTokenCount > 0;
}
function deleteAllTokensForUser(PDO $db, string $userId)
{
$db
->prepare(
<<<SQL
DELETE
FROM send_email_token
WHERE userId = :userId;
SQL
,
)
->execute(["userId" => $userId]);
}
function deleteAllOtpsForUser(PDO $db, string $userId)
{
$db
->prepare(
<<<SQL
DELETE
FROM otp
WHERE userId = :userId;
SQL
,
)
->execute(["userId" => $userId]);
}
function recordSentEmail(PDO $db, string $messageId)
{
$now = time();
$stmt = $db->prepare(
<<<SQL
INSERT
INTO sent_emails
(messageId, sentAtUnixSecs)
VALUES
(:messageId, :now);
SQL
,
);
$stmt->execute(["messageId" => $messageId, "now" => $now]);
}
function insertOtp(PDO $db, string $userId, string $otp)
{
$createdAtUnixSecs = time();
$validUntilUnixSecs = $createdAtUnixSecs + 60 * 5;
$db
->prepare(
<<<SQL
INSERT
INTO otp
(userId, value, createdAtUnixSecs, validUntilUnixSecs)
VALUES
(:userId, :otp, :createdAtUnixSecs, :validUntilUnixSecs)
SQL
,
)
->execute([
"userId" => $userId,
"otp" => $otp,
"createdAtUnixSecs" => $createdAtUnixSecs,
"validUntilUnixSecs" => $validUntilUnixSecs,
]);
}
function isOtpCorrect(
PDO $db,
string $userId,
string $guess,
int $leniencySecs,
): bool {
$stmt = $db->prepare(
<<<SQL
SELECT
COUNT(*) as count
FROM otp
WHERE userId = :userId
AND value = :guess
AND validUntilUnixSecs >= :validUntilUnixSecs
SQL
,
);
$stmt->execute([
"userId" => $userId,
"guess" => $guess,
"validUntilUnixSecs" => time() - $leniencySecs,
]);
return $stmt->fetchColumn(0);
}
+35
View File
@@ -0,0 +1,35 @@
<?php namespace JoeacNet\Http\Mail;
use PHPMailer\PHPMailer\PHPMailer;
use JoeacNet\Http\Config;
require_once __DIR__ . "/../vendor/autoload.php";
function sendEmail(
string $toEmail,
string $toName,
string $subject,
string $body,
string $replyToEmail = null,
string $replyToName = null,
): string {
$mail = new PHPMailer();
$mail->isSMTP();
$mail->Host = Config\localSmtpHost();
$mail->Port = Config\localSmtpPort();
$mail->SMTPAuth = true;
$mail->PlainAuth = true;
$mail->Username = Config\localSmtpUser();
$mail->Password = Config\localSmtpPass();
$mail->setFrom(Config\localSmtpFrom(), Config\localSmtpFromName());
if (!is_null($replyToEmail)) {
$mail->addReplyTo($replyToEmail, $replyToName);
}
$mail->addAddress($toEmail, $toName);
$mail->isHTML(false);
$mail->Subject = "joeac.net: $subject";
$mail->Body = "$body";
$mail->send();
return $mail->getLastMessageID();
}
+2
View File
@@ -0,0 +1,2 @@
pp
*.o
+15
View File
@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2021, Adrian Emil Grigore <adrian.emil.grigore@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+29
View File
@@ -0,0 +1,29 @@
CC = cc
LD = ${CC}
PREFIX = /usr/local
MANPREFIX = ${PREFIX}/share/man
CFLAGS = -Wall -Wextra -pedantic
pp: pp.o
pp.o: pp.c
.c.o:
$(CC) $(CPPFLAGS) $< $(CFLAGS) -c -o $@
.o:
$(LD) $< $(LDFLAGS) -o $@
clean:
rm -rf pp *.o
install: pp pp.1
mkdir -p $(DESTDIR)$(PREFIX)/bin
mkdir -p $(DESTDIR)$(MANPREFIX)/man1
cp pp $(DESTDIR)$(PREFIX)/bin/pp
cp pp.1 $(DESTDIR)$(MANPREFIX)/man1/
chmod 755 $(DESTDIR)$(PREFIX)/bin/pp
uninstall:
rm $(DESTDIR)$(PREFIX)/bin/pp
rm $(DESTDIR)$(MANPREFIX)/man1/pp.1
+188
View File
@@ -0,0 +1,188 @@
# `pp(1)`
a preprocessor
`pp(1)` allows embedding
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
code in files of any type by nesting it inside the `#!\n` token, where
`\n` is a new line. That means that if you'd like a simple loop or an
`if` inside an HTML file for instance,
you could use `pp(1)`.
## Basic usage
For the following code:
<!doctype html>
<title>pp(1) example</title>
<ul>
#!
i=1
while test $i -le 10
do
if test $((i % 2)) -eq 0
then
#!
<li class=even>$i</li>
#!
else
#!
<li class=odd>$i</li>
#!
fi
i=$((i + 1))
done
#!
</ul>
`pp(1)` outputs:
<!doctype html>
<title>pp(1) example</title>
<ul>
<li class=odd>1</li>
<li class=even>2</li>
<li class=odd>3</li>
<li class=even>4</li>
<li class=odd>5</li>
<li class=even>6</li>
<li class=odd>7</li>
<li class=even>8</li>
<li class=odd>9</li>
<li class=even>10</li>
</ul>
## Arguments
You can also pass arguments to it by appending them to the command call:
<!doctype html>
<title>$1</title>
<p>
#!
echo $2
#!
</p>
Calling the above code with
`pp tmp.upphtml 'pp(1) example' 'hello, world'`
will result in:
<!doctype html>
<title>pp(1) example</title>
<p>
hello world
</p>
## Pipes
`pp(1)`'s `stdin` is sent to the child
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
<!doctype html>
<title>pp(1) example</title>
<p>
#!
cat
#!
</p>
`echo 'hello, world' | pp tmp.upphtml` will output:
<!doctype html>
<title>pp(1) example</title>
<p>
hello, world
</p>
## Debugging
`pp(1)` also takes an optional `-d` flag. If passed, `pp(1)` will dump
the generated
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
code instead of executing it:
<!doctype html>
<title>pp(1) example</title>
<ul>
#!
i=1
while test $i -le 10
do
if test $((i % 2)) -eq 0
then
#!
<li class=even>$i</li>
#!
else
#!
<li class=odd>$i</li>
#!
fi
i=$((i + 1))
done
#!
</ul>
`pp -d tmp.upphtml` will output:
echo "<!doctype html>
<title>pp(1) example</title>
<ul>"
i=1
while test $i -le 10
do
if test $((i % 2)) -eq 0
then
echo " <li class=even>$i</li>"
else
echo " <li class=odd>$i</li>"
fi
i=$((i + 1))
done
echo "</ul>"
## Example
<!doctype html>
<html lang=en>
<title>pp(1)</title>
<meta charset=UTF-8>
<meta name=viewport content='width=device-width'>
<link rel=stylesheet href=../theme.css>
<body>
<a href=../#myprojects>Home</a>
#!
addcls() {
sed "s/<code>/<code class=language-bash>/g"
}
smu README | addcls
#!
<h2>Files</h2>
<ul>
#!
for f in *
do
case $f in
*.upp*|*.html|*.tgz)
continue
;;
esac
#!
<li><a href=$f>$f</a></li>
#!
done
#!
</ul>
<h2>Download</h2>
<a href=pp.tgz>pp.tgz</a>
<script src=../prism.js data-manual></script>
<script src=../enhance.js></script>
</body>
</html>
## See also
* <https://github.com/radare/spp>
* <https://werc.cat-v.org/docs/rc-template-lang>
* <https://code.9front.org/hg/werc/file/92f7463dac1a/bin/template.awk>
+46
View File
@@ -0,0 +1,46 @@
.Dd Apr 25, 2019
.Dt pp 1
.Os
.Sh NAME
.Nm pp
.Nd preprocessor
.Sh SYNOPSIS
.Nm pp
.Op Fl d
.Ar file
.Op args ...
.Sh DESCRIPTION
The
.Nm
utility processes
.Ar file
to produce output that is used as input to another
program.
.Pp
It executes code enclosed in
.Dq #!\en
delimiters using
.Xr sh 1 .
.Op args ...
are passed to the embbeded script and are available
via the usual
.Em $1, $2, ...
positional parameters.
.Pp
The options are as follows:
.Bl -tag -width Ds
.It Fl d
.Nm
will dump the generated
.Xr sh 1
code instead of executing it. This is useful for debugging.
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
Preprocess file with arguments:
.Pp
.Dl $ pp file.upphtml a b
.Pp
.Sh SEE ALSO
.Xr sh 1
+369
View File
@@ -0,0 +1,369 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#if !__APPLE__
/*
* Appends src to string dst of size dsize (unlike strncat, dsize is the
* full size of dst, not space left). At most dsize-1 characters
* will be copied. Always NUL terminates (unless dsize <= strlen(dst)).
* Returns strlen(src) + MIN(dsize, strlen(initial dst)).
* If retval >= dsize, truncation occurred.
*/
size_t
strlcat(char *dst, const char *src, size_t dsize)
{
const char *odst = dst;
const char *osrc = src;
size_t n = dsize;
size_t dlen;
/* Find the end of dst and adjust bytes left but don't go past end. */
while (n-- != 0 && *dst != '\0')
dst++;
dlen = dst - odst;
n = dsize - dlen;
if (n-- == 0)
return(dlen + strlen(src));
while (*src != '\0') {
if (n != 0) {
*dst++ = *src;
n--;
}
src++;
}
*dst = '\0';
return(dlen + (src - osrc)); /* count does not include NUL */
}
#endif /* !__APPLE__ */
#define MAXSIZE 64000
#define MAXINDENTSIZE 80
#define MAXARGSIZE 20
char *argv0;
int
efgetc(FILE *fp)
{
int c;
c = fgetc(fp);
if (c == EOF &&
ferror(fp) != 0) {
perror(argv0);
exit(1);
}
return c;
}
int
efseek(FILE *fp, long offset, int whence)
{
int s;
s = fseek(fp, offset, whence);
if (s == -1) {
perror(argv0);
exit(1);
}
return s;
}
int
eungetc(int c, FILE *fp)
{
int uc;
uc = ungetc(c, fp);
if (uc == EOF) {
perror(argv0);
exit(1);
}
return uc;
}
size_t
estrlcat(char *dst, const char *src, size_t dstsize)
{
size_t n;
n = strlcat(dst, src, dstsize);
if (n >= dstsize) {
fprintf(stderr, "%s: Buffer overflow\n", argv0);
exit(1);
}
return n;
}
int
eclose(int d)
{
int s;
s = close(d);
if (s == -1) {
perror(argv0);
exit(1);
}
return s;
}
char *
strappend(char *s, char c)
{
size_t len;
len = strlen(s);
s[len] = c;
s[len + 1] = '\0';
return s ;
}
int
shebang(FILE *fp)
{
int c;
c = efgetc(fp);
if (c != '#') {
eungetc(c, fp);
return 0;
}
c = efgetc(fp);
if (c != '!') {
eungetc(c, fp);
eungetc('#', fp);
return 0;
}
c = efgetc(fp);
if (c == '\n') {
eungetc(c, fp);
eungetc('!', fp);
eungetc('#', fp);
return 0;
}
for (;;) {
c = efgetc(fp);
if (c == '\n')
break;
}
return 1;
}
int
token(FILE *fp, int consume)
{
int c;
c = efgetc(fp);
if (c != '#') {
eungetc(c, fp);
return 0;
}
c = efgetc(fp);
if (c != '!') {
eungetc(c, fp);
eungetc('#', fp);
return 0;
};
c = efgetc(fp);
if (c != '\n') {
eungetc(c, fp);
eungetc('!', fp);
eungetc('#', fp);
return 0;
}
if (consume == 0) {
eungetc(c, fp);
eungetc('!', fp);
eungetc('#', fp);
}
return 1;
}
size_t
output(char *s)
{
return estrlcat(s, "echo \"", MAXSIZE);
}
size_t
outputend(char *s)
{
return estrlcat(s, "\"\n", MAXSIZE);
}
int main(int argc, char **argv)
{
char s[MAXSIZE] = "", indent[MAXINDENTSIZE] = "",
*eargv[MAXARGSIZE] = {"sh", "-c", s, "sh", NULL};
FILE *fp;
int size, c, i, fd[2], o = 0, status = 0, d = 0;
argv0 = argv[0];
if (argc == 1) {
fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "-d") == 0) {
if (argc == 2) {
fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
return 1;
}
d = 1;
fp = fopen(argv[2], "r");
} else {
fp = fopen(argv[1], "r");
}
if (!fp) {
fprintf(stderr, "%s: %s: %s\n",
argv[0], d ? argv[2] : argv[1], strerror(errno));
return 1;
}
efseek (fp, 0, SEEK_END);
size = ftell(fp);
if(size == 0) return 0;
efseek(fp, -1, SEEK_END);
c = efgetc(fp);
if (c != '\n') {
fprintf(stderr, "%s: %s: No newline at end of file\n",
argv[0], d ? argv[2] : argv[1]);
exit(1);
}
efseek(fp, 0, SEEK_SET);
shebang(fp);
if (!token(fp, 1)) {
output(s);
o = 1;
}
for(;;) {
if (token(fp, 1)) {
if (!o) {
c = efgetc(fp);
if (c == EOF)
goto done;
eungetc(c, fp);
output(s);
} else
outputend(s);
o = !o;
}
c = efgetc(fp);
if (c == EOF)
break;
if (c == '\n') {
indent[0] = 0;
c = efgetc(fp);
if (c == EOF)
break;
for (;;) {
if (c != ' ' && c != '\t')
break;
strappend(indent, c);
c = efgetc(fp);
if (c == EOF)
break;
}
eungetc(c, fp);
if (token(fp, 0) && o)
continue;
strappend(s, '\n');
estrlcat(s, indent, MAXSIZE);
continue;
}
strappend(s, c);
}
if (o) {
outputend(s);
o = 0;
}
done:
if(d) {
puts(s);
return 0;
}
if (pipe(fd) == -1) {
perror(argv0);
return 1;
}
switch (fork()) {
case -1:
perror(argv0);
return 1;
case 0:
eclose(fd[1]);
eclose(fd[0]);
for (i = 2; i < argc; i++)
eargv[i+2] = argv[i];
if (execv("/bin/sh", eargv) == -1) {
perror(argv0);
return 1;
}
break;
default:
eclose(fd[1]);
eclose(fd[0]);
if (wait(&status) == -1) {
perror(argv0);
return 1;
}
}
return status;
}
+124
View File
@@ -0,0 +1,124 @@
<?php
use JoeacNet\Http\Config;
use JoeacNet\Http\Db;
use JoeacNet\Http\Mail;
require_once __DIR__ . "/../../vendor/autoload.php";
require_once __DIR__ . "/../../php/config.php";
require_once __DIR__ . "/../../php/db.php";
require_once __DIR__ . "/../../php/mail.php";
$db = Db\connectDb();
$payload = json_decode(file_get_contents("php://input"), true);
$name = getNameFromPayloadAndValidate($payload);
$email = getEmailFromPayloadAndValidate($payload);
$message = getMessageFromPayloadAndValidate($payload);
$token = getTokenFromPayloadAndValidate($payload);
$userId = getUserIdFromPayloadAndValidate($payload);
limitMaxDailyEmails($db);
verifyToken(db: $db, userId: $userId, token: $token);
Db\deleteAllTokensForUser($db, $userId);
$messageId = Mail\sendEmail(
subject: "joeac.net: $name left a message",
replyToEmail: $email,
replyToName: $name,
toEmail: Config\contactMailbox(),
toName: Config\contactMailboxName(),
body: "$name <$email> sent you a message:\n\n\n$message",
);
Db\recordSentEmail($db, $messageId);
syslog(LOG_INFO, "Sent an email to Joe. Message ID: $messageId");
http_response_code(200);
echo $messageId;
exit();
/// functions ///
function verifyToken(PDO $db, string $userId, string $token)
{
if (!Db\isTokenValid($db, $userId, $token)) {
http_response_code(400);
echo "token is not valid";
exit();
}
}
function limitMaxDailyEmails(PDO $db)
{
$countEmailsSentLast24Hours = Db\countEmailsSentLast24Hours($db);
if ($countEmailsSentLast24Hours > Config\maxDailyEmails()) {
$msg =
"$countEmailsSentLast24Hours emails have been sent in the last 24 hours, but the max daily load is " .
Config\maxDailyEmails() .
".";
syslog(LOG_WARNING, $msg);
http_response_code(500);
echo $msg;
exit();
}
}
function getNameFromPayloadAndValidate(array $payload): string
{
$name = (string) $payload["name"];
if (is_null($name) || $name == "") {
http_response_code(400);
echo "name must not be empty";
exit();
}
return $name;
}
function getEmailFromPayloadAndValidate(array $payload): string
{
$email = (string) $payload["email"];
if (is_null($email) || $email == "") {
http_response_code(400);
echo "email must not be empty";
exit();
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo "<$email> is not a valid email address.";
exit();
}
return $email;
}
function getMessageFromPayloadAndValidate(array $payload): string
{
$message = (string) $payload["message"];
if (is_null($message) || $message == "") {
http_response_code(400);
echo "message must not be empty";
exit();
}
return $message;
}
function getTokenFromPayloadAndValidate(array $payload): string
{
$token = (string) $payload["token"];
if (is_null($token) || $token == "") {
http_response_code(400);
echo "token must not be empty";
exit();
}
return $token;
}
function getUserIdFromPayloadAndValidate(array $payload): string
{
$userId = (string) $payload["userId"];
if (is_null($userId) || $userId == "") {
http_response_code(400);
echo "userId must not be empty";
exit();
}
return $userId;
}
+104
View File
@@ -0,0 +1,104 @@
<?php
use JoeacNet\Http\Config;
use JoeacNet\Http\Db;
use JoeacNet\Http\Mail;
require_once __DIR__ . "/../../php/config.php";
require_once __DIR__ . "/../../php/db.php";
require_once __DIR__ . "/../../php/mail.php";
const SEND_OTP_TYPES = ["email"];
$db = Db\connectDb();
$payload = json_decode(file_get_contents("php://input"), true);
$email = getEmailFromPayloadAndValidate($payload);
$name = getNameFromPayloadAndValidate($payload);
$type = getTypeFromPayloadAndValidate($payload);
limitMaxDailyEmails(db: $db, email: $email, name: $name);
$otp = strtoupper(bin2hex(random_bytes(3)));
$prettyOtp = substr($otp, 0, 3) . "-" . substr($otp, 3, 6);
Db\insertOtp(db: $db, userId: $email, otp: $otp);
try {
$messageId = Mail\sendEmail(
toEmail: $email,
toName: $name,
subject: "joeac.net: your OTP is $prettyOtp",
body: <<<BODY
Someone tried to use this email address on joeac.net. If this was you,
your one-time passcode is $prettyOtp. If this wasn't you, you don't need
to do anything.
BODY,
);
} catch (Exception $e) {
error_log("Email could not be sent: $e");
http_response_code(500);
echo "Email could not be sent: $e";
exit();
}
Db\recordSentEmail($db, $messageId);
syslog(LOG_INFO, "Sent OTP ($prettyOtp) to $email. Message ID: $messageId");
http_response_code(200);
echo $messageId;
exit();
/// functions ///
function limitMaxDailyEmails(PDO $db, string $email, string $name)
{
$countEmailsSentLast24Hours = Db\countEmailsSentLast24Hours($db);
if ($countEmailsSentLast24Hours > Config\maxDailyEmails()) {
$msg =
"$name <$email> requested an OTP email, but $countEmailsSentLast24Hours emails have already been sent, whereas the max daily load is " .
Config\maxDailyEmails() .
".";
syslog(LOG_WARNING, $msg);
http_response_code(500);
echo $msg;
exit();
}
}
function getNameFromPayloadAndValidate(array $payload): string
{
$name = (string) $payload["name"];
if (is_null($name) || $name == "") {
http_response_code(400);
echo "name must not be empty";
exit();
}
return $name;
}
function getEmailFromPayloadAndValidate(array $payload): string
{
$email = (string) $payload["email"];
if (is_null($email) || $email == "") {
http_response_code(400);
echo "email must not be empty";
exit();
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo "<$email> is not a valid email address.";
exit();
}
return $email;
}
function getTypeFromPayloadAndValidate(array $payload): string
{
$type = (string) $payload["type"];
if (!in_array($type, SEND_OTP_TYPES)) {
http_response_code(400);
echo "type must be one of: " . SEND_OTP_TYPES;
exit();
}
return $type;
}
+63
View File
@@ -0,0 +1,63 @@
<?php
use JoeacNet\Http\Db;
require_once __DIR__ . "/../../php/db.php";
$db = Db\connectDb();
$payload = json_decode(file_get_contents("php://input"), true);
$guess = getGuessFromPayloadAndValidate($payload);
$leniencySecs = getLeniencySecsFromPayloadAndValidate($payload);
$userId = getUserIdFromPayloadAndValidate($payload);
if (
!Db\isOtpCorrect(
db: $db,
userId: $userId,
guess: $guess,
leniencySecs: $leniencySecs,
)
) {
http_response_code(400);
echo "OTP is not valid";
exit();
}
Db\deleteAllOtpsForUser($db, $userId);
$token = bin2hex(random_bytes(256));
Db\insertSendEmailToken(db: $db, userId: $userId, token: $token);
http_response_code(200);
echo $token;
exit();
/// functions ///
function getGuessFromPayloadAndValidate(array $payload): string
{
$guess = (string) $payload["guess"];
if (strlen($guess) != 6) {
http_response_code(400);
echo "guess must be six characters long";
exit();
}
return $guess;
}
function getLeniencySecsFromPayloadAndValidate(array $payload): int
{
if ((bool) $payload["lenient"] ?? false) {
return 60;
}
return 0;
}
function getUserIdFromPayloadAndValidate(array $payload): string
{
$userId = (string) $payload["userId"];
if (is_null($userId) || $userId == "") {
http_response_code(400);
echo "userId must not be empty";
exit();
}
return $userId;
}
+60
View File
@@ -0,0 +1,60 @@
export const actions = {
/**
* @param {{type: 'email', name: string, email: string}}
* @returns {Promise<void>}
*/
sendOtp: async ({ type, name, email }) => {
const url = "/do/send_otp.php";
const req = new Request(url, {
method: "POST",
body: JSON.stringify({ type, name, email }),
});
const res = await fetch(req);
if (!res.ok) {
throw new Error(
`Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`,
);
}
},
/**
* @param {{guess: string, lenient?: bool, userId: string}}
* @returns {Promise<string | false>}
*/
verifyOtp: async ({ guess, lenient, userId }) => {
const url = "/do/verify_otp.php";
const req = new Request(url, {
method: "POST",
body: JSON.stringify({ guess, lenient, userId }),
});
const res = await fetch(req);
if (res.status === 400) {
return false;
} else if (!res.ok) {
throw new Error(
`Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`,
);
}
return res.text();
},
/**
* @param {{email: string, message: string, name: string, userId: string, token: string}}
* @returns {Promise<void>}
*/
sendEmail: async ({ email, message, name, userId, token }) => {
const url = "/do/send_email.php";
const req = new Request(url, {
method: "POST",
body: JSON.stringify({ email, message, name, userId, token }),
});
const res = await fetch(req);
if (!res.ok) {
throw new Error(
`Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`,
);
}
},
};
@@ -0,0 +1,34 @@
/** @typedef {import("./selectors.js").Selectors} Selectors */
/**
* @param {{contactForm: Selectors}} contactForm
* @param {string} errorMsg
*/
export function postErrorMessageOnContactForm({ contactForm }, errorMsg) {
const errorElem = contactForm.errorElem();
if (errorElem) {
errorElem.textContent = errorMsg;
errorElem.removeAttribute("hidden");
} else {
alert(errorMsg);
}
}
/**
* @param {{otpDialog: Selectors}} otpDialog
* @param {string} errorMsg
*/
export function postErrorMessageOnOtpForm({ otpDialog }, errorMsg) {
const errorElem = otpDialog.errorElem();
if (errorElem) {
errorElem.textContent = errorMsg;
errorElem.removeAttribute("hidden");
} else {
alert(errorMsg);
}
/** @type NodeListOf<HTMLInputElement> */
const inputs = otpDialog.allOtpInputs();
for (const input of inputs) {
input.value = "";
}
}
+31
View File
@@ -0,0 +1,31 @@
/** @typedef {import("./selectors.js").Selectors} Selectors */
import { actions } from "./actions.js";
import { resetResendButton } from "./reset-resend-button.js";
import { postErrorMessageOnOtpForm } from "./post-error-message.js";
/** @typedef {{ resendButtonInterval: NodeJS.Timeout | undefined }} Result */
const fallbackErrorMsg =
"No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
/**
* @param {Selectors} selectors
* @param {NodeJS.Timeout | undefined} resetButtonInterval
* @returns {Promise<Result>}
*/
export async function resendOtp(selectors, resetButtonInterval) {
const result = resetResendButton(selectors, resetButtonInterval);
const name = selectors.contactForm.nameElem()?.value;
const email = selectors.contactForm.emailElem().value;
try {
await actions.sendOtp({ type: "email", name, email });
} catch (sendOtpError) {
const errorMsg = sendOtpError?.toString() ?? fallbackErrorMsg;
postErrorMessageOnOtpForm(selectors, errorMsg);
throw sendOtpError;
}
return result;
}
@@ -1,9 +1,13 @@
import type { Selectors } from "./selectors";
/** @typedef {import("./selectors.js").Selectors} Selectors */
export function resetResendButton(
{ otpDialog }: Selectors,
resendButtonInterval: NodeJS.Timeout | undefined,
): Result {
/** @typedef {{ resendButtonInterval: NodeJS.Timeout | undefined }} Result */
/**
* @param {Selectors} selectors
* @param {NodeJS.Timeout | undefined} resetButtonInterval
* @returns {Result}
*/
export function resetResendButton({ otpDialog }, resendButtonInterval) {
clearInterval(resendButtonInterval);
const resendButton = otpDialog.resendButton();
@@ -26,7 +30,3 @@ export function resetResendButton(
return { resendButtonInterval };
}
type Result = {
resendButtonInterval: NodeJS.Timeout | undefined;
};
+25
View File
@@ -0,0 +1,25 @@
/**
* @typedef {Object} Selectors
* @property {Object} contactForm
* @property {() => HTMLInputElement} contactForm.emailElem
* @property {() => Element | null} contactForm.errorElem
* @property {() => HTMLInputElement} contactForm.nameElem
* @property {() => HTMLTextAreaElement} contactForm.messageElem
* @property {() => HTMLFormElement | null} contactForm.self
* @property {() => HTMLInputElement | null} contactForm.submitButton
* @property {Object} otpDialog
* @property {() => NodeListOf<HTMLInputElement>} otpDialog.allOtpInputs
* @property {() => Element | null} otpDialog.errorElem
* @property {() => HTMLInputElement | null} otpDialog.firstOtpInput
* @property {() => HTMLFormElement | null} otpDialog.otpForm
* @property {() => Element | null} otpDialog.otpRecipient
* @property {() => Element | null} otpDialog.otpValidUntil
* @property {() => HTMLButtonElement | null} otpDialog.resendButton
* @property {() => HTMLDialogElement} otpDialog.self
* @property {() => HTMLInputElement | null} otpDialog.submitButton
* @property {Object} successSection
* @property {() => Element | null} successSection.email
* @property {() => Element | null} successSection.message
* @property {() => Element | null} successSection.name
* @property {() => Element | null} successSection.self
*/
@@ -1,26 +1,30 @@
import { actions } from "astro:actions";
import { postErrorMessageOnContactForm } from "./post-error-message";
import { resetResendButton } from "./reset-resend-button";
import type { Selectors } from "./selectors";
import { actions } from "./actions.js";
import { postErrorMessageOnContactForm } from "./post-error-message.js";
import { resetResendButton } from "./reset-resend-button.js";
/** @typedef {import("./selectors.js").Selectors} Selectors */
const fallbackErrorMsg =
"No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
export async function submitContactForm(
selectors: Selectors,
resendButtonInterval: NodeJS.Timeout | undefined,
): Promise<Result> {
/**
* @param {Selectors} selectors
* @param {NodeJS.Timeout | undefined} resetButtonInterval
* @returns {Promise<Result>}
*/
export async function submitContactForm(selectors, resendButtonInterval) {
const { contactForm, otpDialog } = selectors;
const name = contactForm.nameElem()?.value;
const email = contactForm.emailElem().value;
contactForm.submitButton()?.setAttribute("disabled", "");
const sendOtpResult = await actions.otp.send({ type: "email", name, email });
if (sendOtpResult.error) {
const errorMsg = sendOtpResult.error?.toString() ?? fallbackErrorMsg;
try {
await actions.sendOtp({ type: "email", name, email });
} catch (sendOtpError) {
const errorMsg = sendOtpError?.toString() ?? fallbackErrorMsg;
postErrorMessageOnContactForm(selectors, errorMsg);
throw sendOtpResult.error;
throw sendOtpError;
}
const otpRecipient = otpDialog.otpRecipient();
@@ -47,7 +51,3 @@ export async function submitContactForm(
return resetResendButton(selectors, resendButtonInterval);
}
type Result = {
resendButtonInterval: NodeJS.Timeout | undefined;
};
@@ -1,14 +1,15 @@
import { actions } from "astro:actions";
import type { Selectors } from "./selectors";
import {
postErrorMessageOnContactForm,
postErrorMessageOnOtpForm,
} from "./post-error-message";
import { actions } from "./actions.js";
import { postErrorMessageOnOtpForm } from "./post-error-message.js";
/** @typedef {import("./selectors.js").Selectors} Selectors */
const fallbackErrorMsg =
"No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
export async function submitOtpForm(selectors: Selectors) {
/**
* @param {Selectors} selectors
*/
export async function submitOtpForm(selectors) {
const { contactForm, otpDialog, successSection } = selectors;
const otpForm = otpDialog.otpForm();
otpDialog.submitButton()?.setAttribute("disabled", "");
@@ -27,32 +28,36 @@ export async function submitOtpForm(selectors: Selectors) {
const email = contactForm.emailElem().value;
const message = contactForm.messageElem()?.value;
const verifyResult = await actions.otp.verify({ guess, userId: email });
if (verifyResult.error) {
let verifyResult;
try {
verifyResult = await actions.verifyOtp({ guess, userId: email });
} catch (verifyError) {
otpDialog.submitButton()?.removeAttribute("disabled");
postErrorMessageOnOtpForm(
selectors,
verifyResult.error?.toString() ?? fallbackErrorMsg,
verifyError?.toString() ?? fallbackErrorMsg,
);
return;
}
if (!verifyResult.data) {
if (verifyResult === false) {
otpDialog.submitButton()?.removeAttribute("disabled");
postErrorMessageOnOtpForm(selectors, "Incorrect OTP. Check your email?");
otpDialog.firstOtpInput()?.focus();
return;
}
const sendmailToken = verifyResult.data;
const sendmailToken = verifyResult;
const sendmailResult = await actions.sendmail({
email,
message: message,
name: name,
userId: email,
token: sendmailToken,
});
if (sendmailResult.error) {
const errorMsg = sendmailResult.error?.toString() ?? fallbackErrorMsg;
try {
await actions.sendEmail({
email,
message: message,
name: name,
userId: email,
token: sendmailToken,
});
} catch (sendEmailError) {
const errorMsg = sendEmailError?.toString() ?? fallbackErrorMsg;
postErrorMessageOnOtpForm(selectors, errorMsg);
otpDialog.submitButton()?.removeAttribute("disabled");
return;
@@ -1,5 +1,6 @@
class OtpForm extends HTMLElement {
observer?: MutationObserver;
/** @type MutationObserver? */
observer;
constructor() {
super();
@@ -19,9 +20,8 @@ class OtpForm extends HTMLElement {
clearInputs() {
console.log("clearing all inputs");
const inputs = this.querySelectorAll(
'input:not([type="submit"])',
) as NodeListOf<HTMLInputElement>;
/** @type NodeListOf<HTMLInputElement> */
const inputs = this.querySelectorAll('input:not([type="submit"])');
for (const input of inputs) {
input.value = "";
}
@@ -29,10 +29,10 @@ class OtpForm extends HTMLElement {
configureInputs() {
this.observer?.disconnect();
const inputs = this.querySelectorAll(
'input:not([type="submit"])',
) as NodeListOf<HTMLInputElement>;
const form = this.querySelector("form") as HTMLFormElement;
/** @type NodeListOf<HTMLInputElement> */
const inputs = this.querySelectorAll('input:not([type="submit"])');
/** @type NodeListOf<HTMLFormElement> */
const form = this.querySelector("form");
for (const input of inputs) {
input.addEventListener("focus", () => {
@@ -42,15 +42,15 @@ class OtpForm extends HTMLElement {
input.addEventListener("input", () => {
if (input.value.length > 0) {
input.value = input.value.slice(0, 1).toLocaleUpperCase();
const nextInput = input.nextElementSibling as HTMLInputElement;
/** @type HTMLInputElement */
const nextInput = input.nextElementSibling;
if (nextInput) {
nextInput.focus();
return;
}
const submitButton = form.querySelector(
'input[type="submit"]',
) as HTMLInputElement;
/** @type HTMLInputElement */
const submitButton = form.querySelector('input[type="submit"]');
submitButton.focus();
let areAllInputsEntered = true;
inputs.forEach((input) => {
@@ -0,0 +1,4 @@
#!
FORMATTED_DATE="$(date --date "${DATE}" +'%A %-d %B %Y')"
echo "<time datetime='${DATE}' class='${CLASS}' ${ATTRIBUTES}>${FORMATTED_DATE}</time>"
#!
@@ -1,15 +1,3 @@
---
interface Props {
title: string;
description?: string;
image?: string;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = '/images/headshot.jpg' } = Astro.props;
---
<!-- IndieWeb -->
<link rel="authorization_endpoint" href="https://indieauth.com/auth">
<link rel="token_endpoint" href="https://tokens.indieauth.com/token">
@@ -25,16 +13,11 @@ const { title, description, image = '/images/headshot.jpg' } = Astro.props;
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<meta name="generator" content="mkws" />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
+11
View File
@@ -0,0 +1,11 @@
#!
if [ -z "${NO_META}" ]; then
#!
<title>${TITLE}</title>
<meta name='title' content='${TITLE}' />
<meta name='description' content='${DESCRIPTION}' />
#!
fi
#!
@@ -1,6 +1,3 @@
---
---
<nav>
<ul>
<li>
@@ -1,7 +1,4 @@
---
---
<script src="../scripts/otp-form-wc.ts"></script>
<script src="/scripts/otp-form-wc.js"></script>
<link rel="stylesheet" href="/css/otp.css" />
<dialog class="otp-dialog">
+66
View File
@@ -0,0 +1,66 @@
#!
PAGE="$1"
SITE_URL="$2"
DATE_PUBLISHED="${DATE_PUBLISHED:-$(basename "${PAGE}" | cut -c1-10)}"
CANONICAL_URL="${SITE_URL}/blog/$(basename "${PAGE}")"
#!
<!doctype html>
<html lang="en-GB">
<head>
#!
pp "${SHARE}"/components/head.upphtml
#!
<link rel="stylesheet" href="/css/post.css" />
</head>
<body>
#!
pp "${SHARE}"/components/navbar.upphtml
#!
<article class='h-entry'>
<aside>
<span>This is a <a href='/blog'>blog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
#!
if [ -z "${DATE_UPDATED}" ]; then
#!
<span>Published:
#!
DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
#!
.</span>
#!
else
#!
<span>Updated:
#!
DATE="${DATE_UPDATED}" CLASS="dt-updated" pp "${SHARE}/components/formatted_date.upphtml"
#!
.</span>
<span>Originally published:
#!
DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
#!
.</span>
#!
fi
#!
<span hidden><a class='u-url uid' href='${CANONICAL_URL}'>Permalink</a></span>
</aside>
<header>
<h1 class='h-name'>${POST_TITLE}</h1>
</header>
<section class='e-content'>
#!
pp "${PAGE}"
#!
</section>
</article>
+20
View File
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en-GB">
<head>
#!
pp "${SHARE}"/components/head.upphtml
#!
</head>
<body>
#!
pp "${SHARE}"/components/navbar.upphtml
#!
<main>
#!
pp "$1" "$2"
#!
</main>
</body>
</html>
@@ -0,0 +1,51 @@
#!
SRC="$1"
SITE_URL="$2"
DATE_PUBLISHED="$(basename "${SRC}" | cut -c1-10)"
CANONICAL_URL="${SITE_URL}/microlog/$(basename "${SRC}")"
#!
<!doctype html>
<html lang="en-GB">
<head>
#!
pp "${SHARE}"/components/head.upphtml
#!
<link rel="stylesheet" href="/css/post.css" />
</head>
<body>
#!
pp "${SHARE}"/components/navbar.upphtml
#!
<article class="h-entry">
<aside>
<span>This is a <a href='/microlog'>microlog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
<span>Published:
#!
DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
#!
.</span>
<span hidden><a class='u-url uid' href='${CANONICAL_URL}'>Permalink</a></span>
</aside>
<header>
<h1 class='h-name'>
#!
echo "${POST_TITLE}"
#!
</h1>
</header>
<section class="e-content">
#!
pp "${SRC}"
#!
</section>
</article>
@@ -0,0 +1,3 @@
#!
pp $@
#!
+55
View File
@@ -0,0 +1,55 @@
.Dd Sep 4, 2020
.Dt lmt
.Os
.Sh NAME
.Nm lmt
.Nd last modification time
.Sh SYNOPSIS
.Nm lmt
.Op Fl f Ar format
.Ar file ...
.Sh DESCRIPTION
The
.Nm
utility prints the last modification time for the given
.Ar file
to standard output.
The information displayed is obtained by calling
.Xr lstat 2
with the given
argument and evaluating the returned structure.
.Pp
The options are as follows:
.Bl -tag -width Ds
.It Fl f Ar format
The format in which to display the time.
The format string may contain any of the conversion specifications
described in the
.Xr strftime 3
manual page, as well as any arbitrary text.
A newline
.Pq Ql \en
character is always output after the characters specified by
the format string.
The format string for the default display is:
.Bd -literal -offset indent
%a %b %e %H:%M:%S %Z %Y
.Ed
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
Print last modification time for
.Pa file1
to standard output:
.Pp
.Dl $ lmt file1
.Pp
Print last modification time for
.Pa file1
to standard output in ISO 8601 format:
.Pp
.Dl $ lmt -f "%Y-%m-%dT%H:%M:%S%z" file1
.Sh SEE ALSO
.Xr stat 1
.Xr strftime 3
+91
View File
@@ -0,0 +1,91 @@
.Dd Dec 13, 2019
.Dt mkws 1
.Os
.Sh NAME
.Nm mkws
.Nd make web site
.Sh SYNOPSIS
.Nm
.Ar url
.Op path
.Sh DESCRIPTION
The
.Nm
utility uses
.Xr pp 1
and the theme files in the
.Ev MKWSTHEMEPATH
directory
to preprocess all files in
.Op path
in order to produce a static web site in the current directory. If no
.Op path
is specified, the current directory is used instead.
.Pp
The options are as follows:
.Bl -tag -width Ds
.It Ar url
The url of the web site. It is used for generating
.Pa sitemap.xml
.
.It Op path
The path where the unpreprocessed files are located. If no
.Op path
is specified, the current directory is used instead.
.El
.Sh ENVIRONMENT
.Bl -tag -width MKWSTHEMEDIR
.It Ev MKWSTHEMEDIR
The directory containing the theme files. See the
.Em FILES
section for the minimum required files for a theme. If no
.Ev MKWSTHEMEDIR
variable is set
.Pa [path]/share
wil be used.
.It Ev LANG
The language for the website. It will be parsed to fill in the
.Em lang
attribute of the
.Em html
element and then
.Em charset
.Em meta
element.
.El
.Sh FILES
.Bl -tag -width "/tmp/vi.recover
.It Pa $MKWSTHEMEDIR/l.upphtml
The layout file of the website. It contains the
.Em DOCTYPE ,
.Em html ,
.Em head ,
.Em title ,
.Em meta
,
.Em link
,
.Em body
elements.
.It Pa $MKWSTHEMEDIR/sitemap.xml
The
.Pa sitemap.txt
template.
.El
.Sh EXIT STATUS
.Ex -std
.Sh EXAMPLES
Make https://example.com web site using the unpreprocessed files in the
current directory
.Pp
.Dl $ mkws https://example.com
.Pp
Make https://example.com web site using the unpreprocessed files in the
.Ar ~/src/ws
directory
.Pp
.Dl $ mkws https://example.com ~/src/ws
.Sh SEE ALSO
.Xr sh 1
.Xr pp 1
.Xr locale 1
+1
View File
@@ -0,0 +1 @@
../../../pp/pp.1
+76
View File
@@ -0,0 +1,76 @@
#!
PAGE="$1"
SITE_URL="$2"
export TITLE="Joe Carstairs"
export DESCRIPTION="Joe Carstairs' personal website"
POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\.blog.upphtml$")"
set --
for post in $POSTS
do
this_year="$( echo "$post" | cut -c1-4 )"
has_this_year=0
for year in $@
do
if [ "$this_year" = "$year" ]
then
has_this_year=1
break
fi
done
if [ $has_this_year -eq 0 ]
then
set -- $@ "$this_year"
fi
done
YEARS="$@"
pp "${SHARE}"/components/meta.upphtml
#!
<section class='h-feed'>
<h1>My blog</h1>
<aside>
<p>This blog is written by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</p>
<p hidden><a class='u-url' href='${SITE_URL}/blog'>Permalink</a></p>
</aside>
<section>
<p><a href='/subscribe'>How to subscribe to my logs</a></p>
</section>
#!
for year in $YEARS; do
#!
<h2>${year}</h2>
<ul>
#!
for post in $POSTS; do if [ "$(echo "${post}" | cut -c1-4)" = "$year" ]; then
title="$(grep "^export TITLE=" "${SRC}/blog/${post}" | cut -c15-)"
title="$(echo ${title} | cut -c1-$(( ${#title} - 16 )) )"
date_published="$(echo "${post}" | cut -c1-10)"
href="${SITE_URL}/blog/$(echo "$post" | cut -c1-10).html"
#!
<li class='h-entry'>
<h3><a class='u-url p-name' href='${href}'>${title}</a></h3>
#!
DATE="${date_published}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
#!
</li>
#!
fi; done
#!
</ul>
#!
done
#!
</section>
+50
View File
@@ -0,0 +1,50 @@
#!
SITE_URL="$1"
POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.blog.upphtml$")"
NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
#!
<rss version='2.0'>
<channel>
<title>Joe Carstairs blog</title>
<description>Short posts on random topics I find interesting</description>
<link>${SITE_URL}/blog</link>
<image>${SITE_URL}/images/headshot.webp</image>
<language>en-GB</language>
<pubDate>${NOW_RFC_822}</pubDate>
#!
for post in $POSTS; do
POST_ID="$(echo ${post} | cut -c1-$(( ${#post} - 13 )) )"
. "${SRC}/blog/${post}.args"
DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="$( echo "${POST_ID}" | cut -c1-10)" +"%a, %d %b %Y %H:%M:%S UT")"
#!
<item>
<title>${POST_TITLE}</title>
<link>${SITE_URL}/blog/${POST_ID}</link>
<guid isPermaLink='true'>${SITE_URL}/blog/${POST_ID}</guid>
<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
<description>
#!
NO_META=1 pp "${SRC}/blog/$post" "${SITE_URL}" \
| sed "s/&/\\&amp;/g" \
| sed "s/</\\&lt;/g" \
| sed "s/>/\\&gt;/g" \
| sed "s/\"/\\&quot;/g" \
| sed "s/'/\\&#x27;/g" \
| sed "s/\`/\\&#x60;/g" \
| sed "s/\\\$/\\\\\$/g"
#!
</description>
<author>Joe Carstairs</author>
</item>
#!
done
#!
</channel>
</rss>
+4
View File
@@ -0,0 +1,4 @@
#!
SITE_URL="${1}"
#!
<meta http-equiv=refresh content=0;url=${SITE_URL}/subscribe />
+96
View File
@@ -0,0 +1,96 @@
#!
export TITLE="Contact"
export DESCRIPTION="Contact Joe Carstairs"
pp "${SHARE}"/components/meta.upphtml
#!
#!
pp "${SHARE}"/components/otp-dialog.upphtml
#!
<form class=contact-form>
<h1>Contact me</h1>
<p hidden class=error></p>
<label for=name>Name</label>
<input id=name name=name type=text required>
<label for=email>Email</label>
<input id=email name=email type=email required>
<label for=message>Message</label>
<textarea id=message name=message required></textarea>
<input type=submit value=Send>
</form>
<section class=success hidden=true>
<h1>You sent me a message!</h1>
<p>Thanks for that. I may be in touch.</p>
<p>In case you forgot, your message was this:</p>
<hr />
<dl>
<dt>Name</dt> <dd class=sentname>???</dd>
<dt>Email</dt> <dd class=sentemail>???</dd>
<dt>Message</dt> <dd class=sentmessage>???</dd>
</dl>
</section>
<script type=module>
import { resendOtp } from '/scripts/contact/resend-otp.js';
import { submitContactForm } from '/scripts/contact/submit-contact-form.js';
import { submitOtpForm } from '/scripts/contact/submit-otp-form.js';
function locateOrPanic(selector, desc, root) {
const elem = (root ?? document).querySelector(selector);
if (!elem) {
alert('Technical error: could not locate ' + desc + '. Please let Joe know if you have another means of contacting him.');
throw new Error('Could not locate ' + desc);
}
return elem;
}
const contactForm = locateOrPanic('form.contact-form', 'contact form');
const otpDialog = locateOrPanic('dialog.otp-dialog', 'OTP dialog');
const otpForm = locateOrPanic('form.otp-form', 'OTP form');
const successSection = document.querySelector('section.success');
let resendButtonInterval = undefined;
const selectors = {
contactForm: {
emailElem: () => locateOrPanic('input[name=email]', 'email input', contactForm),
errorElem: () => contactForm.querySelector('.error'),
nameElem: () => locateOrPanic('input[name=name]', 'name input', contactForm),
messageElem: () => locateOrPanic('textarea[name=message]', 'message textarea', contactForm),
self: () => contactForm,
submitButton: () => contactForm.querySelector('input[type=submit]'),
},
otpDialog: {
allOtpInputs: () => otpForm.querySelectorAll('input:not([type=submit])'),
errorElem: () => otpDialog.querySelector('.error'),
firstOtpInput: () => otpForm.querySelector('input:not([type=submit]):first-child'),
otpForm: () => otpForm,
otpRecipient: () => otpDialog.querySelector('.otp-recipient'),
otpValidUntil: () => otpDialog.querySelector('.otp-valid-until'),
resendButton: () => otpDialog.querySelector('button.resend-button'),
self: () => otpDialog,
submitButton: () => otpForm.querySelector('input[type=submit]'),
},
successSection: {
email: () => successSection?.querySelector('.sentemail') ?? null,
name: () => successSection?.querySelector('.sentname') ?? null,
message: () => successSection?.querySelector('.sentmessage') ?? null,
self: () => successSection,
}
};
contactForm.addEventListener('submit', async (event) => {
event.preventDefault();
({ resendButtonInterval } = await submitContactForm(selectors, resendButtonInterval));
});
selectors.otpDialog.resendButton()?.addEventListener('click', async () => {
({ resendButtonInterval } = await resendOtp(selectors, resendButtonInterval));
});
otpForm.addEventListener('submit', async (event) => {
event.preventDefault();
await submitOtpForm(selectors);
});
</script>
@@ -114,7 +114,7 @@ body {
/* Geometric Humanist stack from https://modernfontstacks.com */
font-family:
Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif;
Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif;
}
small {
@@ -305,11 +305,6 @@ strong {
/** Blog feed */
.h-feed ul {
list-style: none;
margin-inline: 0;
}
/** Block quotes */
blockquote {
padding-inline-start: var(--spacing-inline-lg);
@@ -317,6 +312,10 @@ blockquote {
font-style: italic;
}
blockquote :first-child {
margin-block-start: 0;
}
blockquote footer {
font-style: initial;
}
@@ -442,11 +441,11 @@ form {
margin-block-end: var(--spacing-block-xs);
}
* + :is(label, input[type="submit"], button) {
* + :is(label, input[type='submit'], button) {
margin-block-start: var(--spacing-block-md);
}
:is(input[type="submit"], input[type="button"], button) {
:is(input[type='submit'], input[type='button'], button) {
padding-inline: var(--spacing-inline-sm);
max-width: max-content;
&:not(dialog *) {
@@ -454,7 +453,7 @@ form {
}
}
:is(input[type="email"], input[type="text"], select) {
:is(input[type='email'], input[type='text'], select) {
max-width: 100%;
width: 16rem;
}
@@ -3,6 +3,15 @@
margin-block-start: var(--spacing-block-md);
}
.h-feed ul:has(> .h-entry) {
list-style-type: none;
margin-inline: 0;
ul {
list-style-type: disc;
}
}
.h-feed .h-entry {
display: flex;
flex-direction: column;
@@ -32,5 +41,5 @@
}
.h-feed :is(a.full-feed-link, .full-feed-link a)::after {
content: " >";
content: ' >';
}
@@ -27,7 +27,7 @@
top: -6rem;
/* A content value is needed to get the ::after to render */
content: "";
content: '';
}
@media (min-width: 36rem) {
@@ -37,8 +37,8 @@
grid-template-columns: subgrid; /** Subgrid of main column layout */
grid-template-rows: min-content 1fr;
grid-template-areas:
"empty heading"
"photo text";
'empty heading'
'photo text';
column-gap: var(--spacing-block-sm);
}
@@ -23,7 +23,7 @@
margin: 0;
}
input[type="submit"] {
input[type='submit'] {
max-width: max-content;
}
}

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