diff --git a/package.json b/package.json index b4413e2b..f33e3029 100755 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build": "astro build", "preview": "astro preview", "check": "astro check", + "cache-alumni-avatars": "bun scripts/cache-alumni-avatars.mjs", "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro", "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx,.astro --fix", "format": "prettier --write .", diff --git a/public/images/alumni/avatars/npub10wzfa7jkqj6c65xyr93hhxrns37ml9tss82jvymv8fymwdtu6cts3h6pvr.webp b/public/images/alumni/avatars/npub10wzfa7jkqj6c65xyr93hhxrns37ml9tss82jvymv8fymwdtu6cts3h6pvr.webp new file mode 100644 index 00000000..44fc0509 Binary files /dev/null and b/public/images/alumni/avatars/npub10wzfa7jkqj6c65xyr93hhxrns37ml9tss82jvymv8fymwdtu6cts3h6pvr.webp differ diff --git a/public/images/alumni/avatars/npub12rv5lskctqxxs2c8rf2zlzc7xx3qpvzs3w4etgemauy9thegr43sf485vg.webp b/public/images/alumni/avatars/npub12rv5lskctqxxs2c8rf2zlzc7xx3qpvzs3w4etgemauy9thegr43sf485vg.webp new file mode 100644 index 00000000..7fa3d4eb Binary files /dev/null and b/public/images/alumni/avatars/npub12rv5lskctqxxs2c8rf2zlzc7xx3qpvzs3w4etgemauy9thegr43sf485vg.webp differ diff --git a/public/images/alumni/avatars/npub12w53uwny68m936vr4s0yl8svd9lc7vlqrkx7hepe7nq6jggn7kfq92rrm3.webp b/public/images/alumni/avatars/npub12w53uwny68m936vr4s0yl8svd9lc7vlqrkx7hepe7nq6jggn7kfq92rrm3.webp new file mode 100644 index 00000000..2ab25fce Binary files /dev/null and b/public/images/alumni/avatars/npub12w53uwny68m936vr4s0yl8svd9lc7vlqrkx7hepe7nq6jggn7kfq92rrm3.webp differ diff --git a/public/images/alumni/avatars/npub12zpfs3yq7we83yvypgsrw5f88y2fv780c2kfs89ge5qk6q3sfm7spks880.webp b/public/images/alumni/avatars/npub12zpfs3yq7we83yvypgsrw5f88y2fv780c2kfs89ge5qk6q3sfm7spks880.webp new file mode 100644 index 00000000..3f3c868c Binary files /dev/null and b/public/images/alumni/avatars/npub12zpfs3yq7we83yvypgsrw5f88y2fv780c2kfs89ge5qk6q3sfm7spks880.webp differ diff --git a/public/images/alumni/avatars/npub13nfdp7p3pacqn6202q33sur4djeehf50xagxq3y3pchhzjptz7yqenvn7c.webp b/public/images/alumni/avatars/npub13nfdp7p3pacqn6202q33sur4djeehf50xagxq3y3pchhzjptz7yqenvn7c.webp new file mode 100644 index 00000000..57777fe6 Binary files /dev/null and b/public/images/alumni/avatars/npub13nfdp7p3pacqn6202q33sur4djeehf50xagxq3y3pchhzjptz7yqenvn7c.webp differ diff --git a/public/images/alumni/avatars/npub149p5act9a5qm9p47elp8w8h3wpwn2d7s2xecw2ygnrxqp4wgsklq9g722q.webp b/public/images/alumni/avatars/npub149p5act9a5qm9p47elp8w8h3wpwn2d7s2xecw2ygnrxqp4wgsklq9g722q.webp new file mode 100644 index 00000000..55acda46 Binary files /dev/null and b/public/images/alumni/avatars/npub149p5act9a5qm9p47elp8w8h3wpwn2d7s2xecw2ygnrxqp4wgsklq9g722q.webp differ diff --git a/public/images/alumni/avatars/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr.webp b/public/images/alumni/avatars/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr.webp new file mode 100644 index 00000000..820ba3b5 Binary files /dev/null and b/public/images/alumni/avatars/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr.webp differ diff --git a/public/images/alumni/avatars/npub15ymzglvv4alrp06q85eqqmawegxf688v0gtqwhjpgtp0a4k2mesqshkwwx.webp b/public/images/alumni/avatars/npub15ymzglvv4alrp06q85eqqmawegxf688v0gtqwhjpgtp0a4k2mesqshkwwx.webp new file mode 100644 index 00000000..b2b995f1 Binary files /dev/null and b/public/images/alumni/avatars/npub15ymzglvv4alrp06q85eqqmawegxf688v0gtqwhjpgtp0a4k2mesqshkwwx.webp differ diff --git a/public/images/alumni/avatars/npub169d679h2ugm9s3j84w63mf7q7uhhxaw2p3d0w487zadym7gt90cqdqn6cc.webp b/public/images/alumni/avatars/npub169d679h2ugm9s3j84w63mf7q7uhhxaw2p3d0w487zadym7gt90cqdqn6cc.webp new file mode 100644 index 00000000..6161f815 Binary files /dev/null and b/public/images/alumni/avatars/npub169d679h2ugm9s3j84w63mf7q7uhhxaw2p3d0w487zadym7gt90cqdqn6cc.webp differ diff --git a/public/images/alumni/avatars/npub16c9a45p5dr6l3jzmrvgdh9m7xy994tatxd6sm7kmxaygkq4lertsfnacfm.webp b/public/images/alumni/avatars/npub16c9a45p5dr6l3jzmrvgdh9m7xy994tatxd6sm7kmxaygkq4lertsfnacfm.webp new file mode 100644 index 00000000..9c5058e5 Binary files /dev/null and b/public/images/alumni/avatars/npub16c9a45p5dr6l3jzmrvgdh9m7xy994tatxd6sm7kmxaygkq4lertsfnacfm.webp differ diff --git a/public/images/alumni/avatars/npub16dhgpql60vmd4mnydjut87vla23a38j689jssaqlqqlzrtqtd0kqex0nkq.webp b/public/images/alumni/avatars/npub16dhgpql60vmd4mnydjut87vla23a38j689jssaqlqqlzrtqtd0kqex0nkq.webp new file mode 100644 index 00000000..a8d034ba Binary files /dev/null and b/public/images/alumni/avatars/npub16dhgpql60vmd4mnydjut87vla23a38j689jssaqlqqlzrtqtd0kqex0nkq.webp differ diff --git a/public/images/alumni/avatars/npub16p8v7varqwjes5hak6q7mz6pygqm4pwc6gve4mrned3xs8tz42gq7kfhdw.webp b/public/images/alumni/avatars/npub16p8v7varqwjes5hak6q7mz6pygqm4pwc6gve4mrned3xs8tz42gq7kfhdw.webp new file mode 100644 index 00000000..5806e28d Binary files /dev/null and b/public/images/alumni/avatars/npub16p8v7varqwjes5hak6q7mz6pygqm4pwc6gve4mrned3xs8tz42gq7kfhdw.webp differ diff --git a/public/images/alumni/avatars/npub16q4rk48xgvlu0snq36v7lx62akgrjezygmdfsgkjpdde3yykfuhscgxcms.webp b/public/images/alumni/avatars/npub16q4rk48xgvlu0snq36v7lx62akgrjezygmdfsgkjpdde3yykfuhscgxcms.webp new file mode 100644 index 00000000..03ff03e0 Binary files /dev/null and b/public/images/alumni/avatars/npub16q4rk48xgvlu0snq36v7lx62akgrjezygmdfsgkjpdde3yykfuhscgxcms.webp differ diff --git a/public/images/alumni/avatars/npub16vzjeglr653mrmyqvu0trwaq29az753wr9th3hyrm5p63kz2zu8qzumhgd.webp b/public/images/alumni/avatars/npub16vzjeglr653mrmyqvu0trwaq29az753wr9th3hyrm5p63kz2zu8qzumhgd.webp new file mode 100644 index 00000000..78836c94 Binary files /dev/null and b/public/images/alumni/avatars/npub16vzjeglr653mrmyqvu0trwaq29az753wr9th3hyrm5p63kz2zu8qzumhgd.webp differ diff --git a/public/images/alumni/avatars/npub175aemydge5thld9peuyp5xmdtp6e5wq77ys203dp3s88pjhgpxpsvgkemm.webp b/public/images/alumni/avatars/npub175aemydge5thld9peuyp5xmdtp6e5wq77ys203dp3s88pjhgpxpsvgkemm.webp new file mode 100644 index 00000000..b5b68b1f Binary files /dev/null and b/public/images/alumni/avatars/npub175aemydge5thld9peuyp5xmdtp6e5wq77ys203dp3s88pjhgpxpsvgkemm.webp differ diff --git a/public/images/alumni/avatars/npub17ngcvm59n9trc5kwam03rs5ts4n7gewxax53m7f2m4f464ls92cqr5qjta.webp b/public/images/alumni/avatars/npub17ngcvm59n9trc5kwam03rs5ts4n7gewxax53m7f2m4f464ls92cqr5qjta.webp new file mode 100644 index 00000000..34c575af Binary files /dev/null and b/public/images/alumni/avatars/npub17ngcvm59n9trc5kwam03rs5ts4n7gewxax53m7f2m4f464ls92cqr5qjta.webp differ diff --git a/public/images/alumni/avatars/npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf.webp b/public/images/alumni/avatars/npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf.webp new file mode 100644 index 00000000..5296be37 Binary files /dev/null and b/public/images/alumni/avatars/npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf.webp differ diff --git a/public/images/alumni/avatars/npub18gr2m5cflkzpn6jdfer4a8qdlavsn334m9mfhurjsge08grg82zq6hu9su.webp b/public/images/alumni/avatars/npub18gr2m5cflkzpn6jdfer4a8qdlavsn334m9mfhurjsge08grg82zq6hu9su.webp new file mode 100644 index 00000000..7a0591c8 Binary files /dev/null and b/public/images/alumni/avatars/npub18gr2m5cflkzpn6jdfer4a8qdlavsn334m9mfhurjsge08grg82zq6hu9su.webp differ diff --git a/public/images/alumni/avatars/npub18r5cznu8e4632p4w7htahuypmc226vrudxz9y64se6heda35jdescaeqct.webp b/public/images/alumni/avatars/npub18r5cznu8e4632p4w7htahuypmc226vrudxz9y64se6heda35jdescaeqct.webp new file mode 100644 index 00000000..5b5349f3 Binary files /dev/null and b/public/images/alumni/avatars/npub18r5cznu8e4632p4w7htahuypmc226vrudxz9y64se6heda35jdescaeqct.webp differ diff --git a/public/images/alumni/avatars/npub18vay956v7zs5qtgc65mvn54v96cuvqv6j9fmu4cgfjqkt5vjuvjsc47nzf.webp b/public/images/alumni/avatars/npub18vay956v7zs5qtgc65mvn54v96cuvqv6j9fmu4cgfjqkt5vjuvjsc47nzf.webp new file mode 100644 index 00000000..7fd9ebf8 Binary files /dev/null and b/public/images/alumni/avatars/npub18vay956v7zs5qtgc65mvn54v96cuvqv6j9fmu4cgfjqkt5vjuvjsc47nzf.webp differ diff --git a/public/images/alumni/avatars/npub19at4nqjjymca2lh3v546vcg5ktun3al4c5xu2pts3ewckv0y70ys0gupzr.webp b/public/images/alumni/avatars/npub19at4nqjjymca2lh3v546vcg5ktun3al4c5xu2pts3ewckv0y70ys0gupzr.webp new file mode 100644 index 00000000..e572c7bc Binary files /dev/null and b/public/images/alumni/avatars/npub19at4nqjjymca2lh3v546vcg5ktun3al4c5xu2pts3ewckv0y70ys0gupzr.webp differ diff --git a/public/images/alumni/avatars/npub19wavu4f7l6l43h24jyskn7fvzy37kcfp67aqjtmv2qgy4lp34nhsda8p6k.webp b/public/images/alumni/avatars/npub19wavu4f7l6l43h24jyskn7fvzy37kcfp67aqjtmv2qgy4lp34nhsda8p6k.webp new file mode 100644 index 00000000..2fed2af3 Binary files /dev/null and b/public/images/alumni/avatars/npub19wavu4f7l6l43h24jyskn7fvzy37kcfp67aqjtmv2qgy4lp34nhsda8p6k.webp differ diff --git a/public/images/alumni/avatars/npub1a3um269aaf3u5cy37kuykrrrnsg2pyv7za06pxjduv25lq5sdujs2qmdj6.webp b/public/images/alumni/avatars/npub1a3um269aaf3u5cy37kuykrrrnsg2pyv7za06pxjduv25lq5sdujs2qmdj6.webp new file mode 100644 index 00000000..1e17ece0 Binary files /dev/null and b/public/images/alumni/avatars/npub1a3um269aaf3u5cy37kuykrrrnsg2pyv7za06pxjduv25lq5sdujs2qmdj6.webp differ diff --git a/public/images/alumni/avatars/npub1aljazgxlpnpfp7n5sunlk3dvfp72456x6nezjw4sd850q879rxqsthg9jp.webp b/public/images/alumni/avatars/npub1aljazgxlpnpfp7n5sunlk3dvfp72456x6nezjw4sd850q879rxqsthg9jp.webp new file mode 100644 index 00000000..faa9301c Binary files /dev/null and b/public/images/alumni/avatars/npub1aljazgxlpnpfp7n5sunlk3dvfp72456x6nezjw4sd850q879rxqsthg9jp.webp differ diff --git a/public/images/alumni/avatars/npub1d7exvqfvxqyrq0j54e23gz6xj4lfj7qfssqamg60fkfp5f6mlzaskklrf3.webp b/public/images/alumni/avatars/npub1d7exvqfvxqyrq0j54e23gz6xj4lfj7qfssqamg60fkfp5f6mlzaskklrf3.webp new file mode 100644 index 00000000..ba596da2 Binary files /dev/null and b/public/images/alumni/avatars/npub1d7exvqfvxqyrq0j54e23gz6xj4lfj7qfssqamg60fkfp5f6mlzaskklrf3.webp differ diff --git a/public/images/alumni/avatars/npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc.webp b/public/images/alumni/avatars/npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc.webp new file mode 100644 index 00000000..becb83df Binary files /dev/null and b/public/images/alumni/avatars/npub1dergggklka99wwrs92yz8wdjs952h2ux2ha2ed598ngwu9w7a6fsh9xzpc.webp differ diff --git a/public/images/alumni/avatars/npub1dmhju6xrn8y09malwrvrrs4krrt6sj7l6gtnf2q7d47nmqtldpgqgctgtm.webp b/public/images/alumni/avatars/npub1dmhju6xrn8y09malwrvrrs4krrt6sj7l6gtnf2q7d47nmqtldpgqgctgtm.webp new file mode 100644 index 00000000..02349a0d Binary files /dev/null and b/public/images/alumni/avatars/npub1dmhju6xrn8y09malwrvrrs4krrt6sj7l6gtnf2q7d47nmqtldpgqgctgtm.webp differ diff --git a/public/images/alumni/avatars/npub1dvdcmtp5llrp63jdlmhspe9gffsyu9ew7cu3ld3f9y7k79nxzjxqf4d4rm.webp b/public/images/alumni/avatars/npub1dvdcmtp5llrp63jdlmhspe9gffsyu9ew7cu3ld3f9y7k79nxzjxqf4d4rm.webp new file mode 100644 index 00000000..eca983dc Binary files /dev/null and b/public/images/alumni/avatars/npub1dvdcmtp5llrp63jdlmhspe9gffsyu9ew7cu3ld3f9y7k79nxzjxqf4d4rm.webp differ diff --git a/public/images/alumni/avatars/npub1e5tfhk8m65teu25dfx8lcvwn4c8ypqjl72u2sh4rt8zy2kss0j5qct7mh9.webp b/public/images/alumni/avatars/npub1e5tfhk8m65teu25dfx8lcvwn4c8ypqjl72u2sh4rt8zy2kss0j5qct7mh9.webp new file mode 100644 index 00000000..358420e4 Binary files /dev/null and b/public/images/alumni/avatars/npub1e5tfhk8m65teu25dfx8lcvwn4c8ypqjl72u2sh4rt8zy2kss0j5qct7mh9.webp differ diff --git a/public/images/alumni/avatars/npub1elta7cneng3w8p9y4dw633qzdjr4kyvaparuyuttyrx6e8xp7xnq32cume.webp b/public/images/alumni/avatars/npub1elta7cneng3w8p9y4dw633qzdjr4kyvaparuyuttyrx6e8xp7xnq32cume.webp new file mode 100644 index 00000000..1a681cf2 Binary files /dev/null and b/public/images/alumni/avatars/npub1elta7cneng3w8p9y4dw633qzdjr4kyvaparuyuttyrx6e8xp7xnq32cume.webp differ diff --git a/public/images/alumni/avatars/npub1equrmqway3qxw3dkssymusxkwgwrqypfgeqx0lx9pgjam7gnj4ysaqhkj6.webp b/public/images/alumni/avatars/npub1equrmqway3qxw3dkssymusxkwgwrqypfgeqx0lx9pgjam7gnj4ysaqhkj6.webp new file mode 100644 index 00000000..8d293a59 Binary files /dev/null and b/public/images/alumni/avatars/npub1equrmqway3qxw3dkssymusxkwgwrqypfgeqx0lx9pgjam7gnj4ysaqhkj6.webp differ diff --git a/public/images/alumni/avatars/npub1ez8efu9rjxu64g0lalty2ffmr2tgkzjz92rkafyfyz5463wr8ars8zls5t.webp b/public/images/alumni/avatars/npub1ez8efu9rjxu64g0lalty2ffmr2tgkzjz92rkafyfyz5463wr8ars8zls5t.webp new file mode 100644 index 00000000..f4e739b4 Binary files /dev/null and b/public/images/alumni/avatars/npub1ez8efu9rjxu64g0lalty2ffmr2tgkzjz92rkafyfyz5463wr8ars8zls5t.webp differ diff --git a/public/images/alumni/avatars/npub1fcthj78n5re7dcyrva89gmvgz0z6g2v5rakr65shxemyvjwwdjzqcpeavj.webp b/public/images/alumni/avatars/npub1fcthj78n5re7dcyrva89gmvgz0z6g2v5rakr65shxemyvjwwdjzqcpeavj.webp new file mode 100644 index 00000000..402fc272 Binary files /dev/null and b/public/images/alumni/avatars/npub1fcthj78n5re7dcyrva89gmvgz0z6g2v5rakr65shxemyvjwwdjzqcpeavj.webp differ diff --git a/public/images/alumni/avatars/npub1ftt05tgku25m2akgvw6v7aqy5ux5mseqcrzy05g26ml43xf74nyqsredsh.webp b/public/images/alumni/avatars/npub1ftt05tgku25m2akgvw6v7aqy5ux5mseqcrzy05g26ml43xf74nyqsredsh.webp new file mode 100644 index 00000000..92a687ab Binary files /dev/null and b/public/images/alumni/avatars/npub1ftt05tgku25m2akgvw6v7aqy5ux5mseqcrzy05g26ml43xf74nyqsredsh.webp differ diff --git a/public/images/alumni/avatars/npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk.webp b/public/images/alumni/avatars/npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk.webp new file mode 100644 index 00000000..0ce439b0 Binary files /dev/null and b/public/images/alumni/avatars/npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk.webp differ diff --git a/public/images/alumni/avatars/npub1g5ax26gr5qcnjh29puccyydxa32v67gyn2z3lykdvupvvhl47k7spf8ypc.webp b/public/images/alumni/avatars/npub1g5ax26gr5qcnjh29puccyydxa32v67gyn2z3lykdvupvvhl47k7spf8ypc.webp new file mode 100644 index 00000000..560c01a6 Binary files /dev/null and b/public/images/alumni/avatars/npub1g5ax26gr5qcnjh29puccyydxa32v67gyn2z3lykdvupvvhl47k7spf8ypc.webp differ diff --git a/public/images/alumni/avatars/npub1gz8kx67jdlx972vgjqemg37tysglvz43hzjler9nssk6kav0m66sv7xjkt.webp b/public/images/alumni/avatars/npub1gz8kx67jdlx972vgjqemg37tysglvz43hzjler9nssk6kav0m66sv7xjkt.webp new file mode 100644 index 00000000..a470f537 Binary files /dev/null and b/public/images/alumni/avatars/npub1gz8kx67jdlx972vgjqemg37tysglvz43hzjler9nssk6kav0m66sv7xjkt.webp differ diff --git a/public/images/alumni/avatars/npub1gzuushllat7pet0ccv9yuhygvc8ldeyhrgxuwg744dn5khnpk3gs3ea5ds.webp b/public/images/alumni/avatars/npub1gzuushllat7pet0ccv9yuhygvc8ldeyhrgxuwg744dn5khnpk3gs3ea5ds.webp new file mode 100644 index 00000000..af71563c Binary files /dev/null and b/public/images/alumni/avatars/npub1gzuushllat7pet0ccv9yuhygvc8ldeyhrgxuwg744dn5khnpk3gs3ea5ds.webp differ diff --git a/public/images/alumni/avatars/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr.webp b/public/images/alumni/avatars/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr.webp new file mode 100644 index 00000000..ff6403fd Binary files /dev/null and b/public/images/alumni/avatars/npub1hw6amg8p24ne08c9gdq8hhpqx0t0pwanpae9z25crn7m9uy7yarse465gr.webp differ diff --git a/public/images/alumni/avatars/npub1jmm9yfymp9rwz46a0z5tcazszg7gue83c4hkktunhs3lkfy7mpdqx6zden.webp b/public/images/alumni/avatars/npub1jmm9yfymp9rwz46a0z5tcazszg7gue83c4hkktunhs3lkfy7mpdqx6zden.webp new file mode 100644 index 00000000..24e0006e Binary files /dev/null and b/public/images/alumni/avatars/npub1jmm9yfymp9rwz46a0z5tcazszg7gue83c4hkktunhs3lkfy7mpdqx6zden.webp differ diff --git a/public/images/alumni/avatars/npub1jss47s4fvv6usl7tn6yp5zamv2u60923ncgfea0e6thkza5p7c3q0afmzy.webp b/public/images/alumni/avatars/npub1jss47s4fvv6usl7tn6yp5zamv2u60923ncgfea0e6thkza5p7c3q0afmzy.webp new file mode 100644 index 00000000..4e352b15 Binary files /dev/null and b/public/images/alumni/avatars/npub1jss47s4fvv6usl7tn6yp5zamv2u60923ncgfea0e6thkza5p7c3q0afmzy.webp differ diff --git a/public/images/alumni/avatars/npub1klkk3vrzme455yh9rl2jshq7rc8dpegj3ndf82c3ks2sk40dxt7qulx3vt.webp b/public/images/alumni/avatars/npub1klkk3vrzme455yh9rl2jshq7rc8dpegj3ndf82c3ks2sk40dxt7qulx3vt.webp new file mode 100644 index 00000000..4d59a198 Binary files /dev/null and b/public/images/alumni/avatars/npub1klkk3vrzme455yh9rl2jshq7rc8dpegj3ndf82c3ks2sk40dxt7qulx3vt.webp differ diff --git a/public/images/alumni/avatars/npub1klr0dy2ul2dx9llk58czvpx73rprcmrvd5dc7ck8esg8f8es06qs427gxc.webp b/public/images/alumni/avatars/npub1klr0dy2ul2dx9llk58czvpx73rprcmrvd5dc7ck8esg8f8es06qs427gxc.webp new file mode 100644 index 00000000..22c9e701 Binary files /dev/null and b/public/images/alumni/avatars/npub1klr0dy2ul2dx9llk58czvpx73rprcmrvd5dc7ck8esg8f8es06qs427gxc.webp differ diff --git a/public/images/alumni/avatars/npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft.webp b/public/images/alumni/avatars/npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft.webp new file mode 100644 index 00000000..051aa9cd Binary files /dev/null and b/public/images/alumni/avatars/npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft.webp differ diff --git a/public/images/alumni/avatars/npub1l5u0zd00va02ch5n6ke2wwxyzamuy5qcsqcu4uwu7pa3dpapleysf7aap5.webp b/public/images/alumni/avatars/npub1l5u0zd00va02ch5n6ke2wwxyzamuy5qcsqcu4uwu7pa3dpapleysf7aap5.webp new file mode 100644 index 00000000..4b77d77b Binary files /dev/null and b/public/images/alumni/avatars/npub1l5u0zd00va02ch5n6ke2wwxyzamuy5qcsqcu4uwu7pa3dpapleysf7aap5.webp differ diff --git a/public/images/alumni/avatars/npub1lg7fsn24xm47yx7yr7xl2wgwjw2wcn06daxtverq068dtgwkd98sf6ge63.webp b/public/images/alumni/avatars/npub1lg7fsn24xm47yx7yr7xl2wgwjw2wcn06daxtverq068dtgwkd98sf6ge63.webp new file mode 100644 index 00000000..4fd9efd1 Binary files /dev/null and b/public/images/alumni/avatars/npub1lg7fsn24xm47yx7yr7xl2wgwjw2wcn06daxtverq068dtgwkd98sf6ge63.webp differ diff --git a/public/images/alumni/avatars/npub1lunaq893u4hmtpvqxpk8hfmtkqmm7ggutdtnc4hyuux2skr4ttcqr827lj.webp b/public/images/alumni/avatars/npub1lunaq893u4hmtpvqxpk8hfmtkqmm7ggutdtnc4hyuux2skr4ttcqr827lj.webp new file mode 100644 index 00000000..fb8254e1 Binary files /dev/null and b/public/images/alumni/avatars/npub1lunaq893u4hmtpvqxpk8hfmtkqmm7ggutdtnc4hyuux2skr4ttcqr827lj.webp differ diff --git a/public/images/alumni/avatars/npub1mgvwnpsqgrem7jfcwm7pdvdfz2h95mm04r23t8pau2uzxwsdnpgs0gpdjc.webp b/public/images/alumni/avatars/npub1mgvwnpsqgrem7jfcwm7pdvdfz2h95mm04r23t8pau2uzxwsdnpgs0gpdjc.webp new file mode 100644 index 00000000..0863bf46 Binary files /dev/null and b/public/images/alumni/avatars/npub1mgvwnpsqgrem7jfcwm7pdvdfz2h95mm04r23t8pau2uzxwsdnpgs0gpdjc.webp differ diff --git a/public/images/alumni/avatars/npub1mhcr4j594hsrnen594d7700n2t03n8gdx83zhxzculk6sh9nhwlq7uc226.webp b/public/images/alumni/avatars/npub1mhcr4j594hsrnen594d7700n2t03n8gdx83zhxzculk6sh9nhwlq7uc226.webp new file mode 100644 index 00000000..6b6ec0d9 Binary files /dev/null and b/public/images/alumni/avatars/npub1mhcr4j594hsrnen594d7700n2t03n8gdx83zhxzculk6sh9nhwlq7uc226.webp differ diff --git a/public/images/alumni/avatars/npub1mqa4auvfmalgs338999h22tf23upfs70uwyetneq0szquqamu7jqkhlfa7.webp b/public/images/alumni/avatars/npub1mqa4auvfmalgs338999h22tf23upfs70uwyetneq0szquqamu7jqkhlfa7.webp new file mode 100644 index 00000000..056dfe39 Binary files /dev/null and b/public/images/alumni/avatars/npub1mqa4auvfmalgs338999h22tf23upfs70uwyetneq0szquqamu7jqkhlfa7.webp differ diff --git a/public/images/alumni/avatars/npub1mutnyacc9uc4t5mmxvpprwsauj5p2qxq95v4a9j0jxl8wnkfvuyque23vg.webp b/public/images/alumni/avatars/npub1mutnyacc9uc4t5mmxvpprwsauj5p2qxq95v4a9j0jxl8wnkfvuyque23vg.webp new file mode 100644 index 00000000..751adcb6 Binary files /dev/null and b/public/images/alumni/avatars/npub1mutnyacc9uc4t5mmxvpprwsauj5p2qxq95v4a9j0jxl8wnkfvuyque23vg.webp differ diff --git a/public/images/alumni/avatars/npub1mygerccwqpzyh9pvp6pv44rskv40zutkfs38t0hqhkvnwlhagp6s3psn5p.webp b/public/images/alumni/avatars/npub1mygerccwqpzyh9pvp6pv44rskv40zutkfs38t0hqhkvnwlhagp6s3psn5p.webp new file mode 100644 index 00000000..7eb646eb Binary files /dev/null and b/public/images/alumni/avatars/npub1mygerccwqpzyh9pvp6pv44rskv40zutkfs38t0hqhkvnwlhagp6s3psn5p.webp differ diff --git a/public/images/alumni/avatars/npub1mz3vx0ew9le6n48l9f2e8u745k0fzel6thksv0gwfxy3wanprcxq79mymx.webp b/public/images/alumni/avatars/npub1mz3vx0ew9le6n48l9f2e8u745k0fzel6thksv0gwfxy3wanprcxq79mymx.webp new file mode 100644 index 00000000..1b1e5818 Binary files /dev/null and b/public/images/alumni/avatars/npub1mz3vx0ew9le6n48l9f2e8u745k0fzel6thksv0gwfxy3wanprcxq79mymx.webp differ diff --git a/public/images/alumni/avatars/npub1n3w4uts2r4srq3adqu9trp953dflcnw7ppn72tad4htkpsckwcmqjef6um.webp b/public/images/alumni/avatars/npub1n3w4uts2r4srq3adqu9trp953dflcnw7ppn72tad4htkpsckwcmqjef6um.webp new file mode 100644 index 00000000..01092fd1 Binary files /dev/null and b/public/images/alumni/avatars/npub1n3w4uts2r4srq3adqu9trp953dflcnw7ppn72tad4htkpsckwcmqjef6um.webp differ diff --git a/public/images/alumni/avatars/npub1nmr6w7qk0ta36vxysv77jv3d5rqghfc6d8sez8240rf3gja4vsmsd2yha8.webp b/public/images/alumni/avatars/npub1nmr6w7qk0ta36vxysv77jv3d5rqghfc6d8sez8240rf3gja4vsmsd2yha8.webp new file mode 100644 index 00000000..578ffb9e Binary files /dev/null and b/public/images/alumni/avatars/npub1nmr6w7qk0ta36vxysv77jv3d5rqghfc6d8sez8240rf3gja4vsmsd2yha8.webp differ diff --git a/public/images/alumni/avatars/npub1nqulzcxcj0d2uesusstgupl5du8pa96xl6uy8xndweeckjkn964qjs23sn.webp b/public/images/alumni/avatars/npub1nqulzcxcj0d2uesusstgupl5du8pa96xl6uy8xndweeckjkn964qjs23sn.webp new file mode 100644 index 00000000..786226b3 Binary files /dev/null and b/public/images/alumni/avatars/npub1nqulzcxcj0d2uesusstgupl5du8pa96xl6uy8xndweeckjkn964qjs23sn.webp differ diff --git a/public/images/alumni/avatars/npub1nyxg7ps82r86u0gunspsn8u8uuskh6sut77tulcqljue7rr7m6hquzh9ph.webp b/public/images/alumni/avatars/npub1nyxg7ps82r86u0gunspsn8u8uuskh6sut77tulcqljue7rr7m6hquzh9ph.webp new file mode 100644 index 00000000..35edd0a9 Binary files /dev/null and b/public/images/alumni/avatars/npub1nyxg7ps82r86u0gunspsn8u8uuskh6sut77tulcqljue7rr7m6hquzh9ph.webp differ diff --git a/public/images/alumni/avatars/npub1p4kg8zxukpym3h20erfa3samj00rm2gt4q5wfuyu3tg0x3jg3gesvncxf8.webp b/public/images/alumni/avatars/npub1p4kg8zxukpym3h20erfa3samj00rm2gt4q5wfuyu3tg0x3jg3gesvncxf8.webp new file mode 100644 index 00000000..1db16702 Binary files /dev/null and b/public/images/alumni/avatars/npub1p4kg8zxukpym3h20erfa3samj00rm2gt4q5wfuyu3tg0x3jg3gesvncxf8.webp differ diff --git a/public/images/alumni/avatars/npub1pmhevxtlt3478pvmdqt7dftnv6zc2mzpdc569yjm6ks4k2jhezcs53uksr.webp b/public/images/alumni/avatars/npub1pmhevxtlt3478pvmdqt7dftnv6zc2mzpdc569yjm6ks4k2jhezcs53uksr.webp new file mode 100644 index 00000000..3e099813 Binary files /dev/null and b/public/images/alumni/avatars/npub1pmhevxtlt3478pvmdqt7dftnv6zc2mzpdc569yjm6ks4k2jhezcs53uksr.webp differ diff --git a/public/images/alumni/avatars/npub1q2s36929z99tv0pfjkqf8jgmn7yxrrjklmsr0wwjl2707vhk965sp7dx3u.webp b/public/images/alumni/avatars/npub1q2s36929z99tv0pfjkqf8jgmn7yxrrjklmsr0wwjl2707vhk965sp7dx3u.webp new file mode 100644 index 00000000..cd0c189e Binary files /dev/null and b/public/images/alumni/avatars/npub1q2s36929z99tv0pfjkqf8jgmn7yxrrjklmsr0wwjl2707vhk965sp7dx3u.webp differ diff --git a/public/images/alumni/avatars/npub1q4hnxfzu5nxylg7p6mj40hvw4ec53z0ncdpre0t0huyl8s8zqrfquvz7zr.webp b/public/images/alumni/avatars/npub1q4hnxfzu5nxylg7p6mj40hvw4ec53z0ncdpre0t0huyl8s8zqrfquvz7zr.webp new file mode 100644 index 00000000..4629361e Binary files /dev/null and b/public/images/alumni/avatars/npub1q4hnxfzu5nxylg7p6mj40hvw4ec53z0ncdpre0t0huyl8s8zqrfquvz7zr.webp differ diff --git a/public/images/alumni/avatars/npub1q6mcr8tlr3l4gus3sfnw6772s7zae6hqncmw5wj27ejud5wcxf7q0nx7d5.webp b/public/images/alumni/avatars/npub1q6mcr8tlr3l4gus3sfnw6772s7zae6hqncmw5wj27ejud5wcxf7q0nx7d5.webp new file mode 100644 index 00000000..b3b345c3 Binary files /dev/null and b/public/images/alumni/avatars/npub1q6mcr8tlr3l4gus3sfnw6772s7zae6hqncmw5wj27ejud5wcxf7q0nx7d5.webp differ diff --git a/public/images/alumni/avatars/npub1qjgcmlpkeyl8mdkvp4s0xls4ytcux6my606tgfx9xttut907h0zs76lgjw.webp b/public/images/alumni/avatars/npub1qjgcmlpkeyl8mdkvp4s0xls4ytcux6my606tgfx9xttut907h0zs76lgjw.webp new file mode 100644 index 00000000..f2cfa1bc Binary files /dev/null and b/public/images/alumni/avatars/npub1qjgcmlpkeyl8mdkvp4s0xls4ytcux6my606tgfx9xttut907h0zs76lgjw.webp differ diff --git a/public/images/alumni/avatars/npub1qnw27f2jsqvn05wzpd56m7ykgmepk57p0yrzw8fzc7lfhjkmjmqqmd9r6h.webp b/public/images/alumni/avatars/npub1qnw27f2jsqvn05wzpd56m7ykgmepk57p0yrzw8fzc7lfhjkmjmqqmd9r6h.webp new file mode 100644 index 00000000..7cf4e270 Binary files /dev/null and b/public/images/alumni/avatars/npub1qnw27f2jsqvn05wzpd56m7ykgmepk57p0yrzw8fzc7lfhjkmjmqqmd9r6h.webp differ diff --git a/public/images/alumni/avatars/npub1r7psmkr4zv93xnal8un6d8hvmpsn5jvhfzn3kk38rfcel6awznks7znspg.webp b/public/images/alumni/avatars/npub1r7psmkr4zv93xnal8un6d8hvmpsn5jvhfzn3kk38rfcel6awznks7znspg.webp new file mode 100644 index 00000000..95ba617c Binary files /dev/null and b/public/images/alumni/avatars/npub1r7psmkr4zv93xnal8un6d8hvmpsn5jvhfzn3kk38rfcel6awznks7znspg.webp differ diff --git a/public/images/alumni/avatars/npub1rtcjydw22laa0ymwatlrtv02lgu6q6sez4gvs63d696putzyezqs65qqsd.webp b/public/images/alumni/avatars/npub1rtcjydw22laa0ymwatlrtv02lgu6q6sez4gvs63d696putzyezqs65qqsd.webp new file mode 100644 index 00000000..b2690c15 Binary files /dev/null and b/public/images/alumni/avatars/npub1rtcjydw22laa0ymwatlrtv02lgu6q6sez4gvs63d696putzyezqs65qqsd.webp differ diff --git a/public/images/alumni/avatars/npub1tlhjzdhlskz9xvmkufucl6l3jt4nypj8q5d2dc572uhz94nyeytq02tuyu.webp b/public/images/alumni/avatars/npub1tlhjzdhlskz9xvmkufucl6l3jt4nypj8q5d2dc572uhz94nyeytq02tuyu.webp new file mode 100644 index 00000000..f4be97c9 Binary files /dev/null and b/public/images/alumni/avatars/npub1tlhjzdhlskz9xvmkufucl6l3jt4nypj8q5d2dc572uhz94nyeytq02tuyu.webp differ diff --git a/public/images/alumni/avatars/npub1u07xw079lxwv24xslarh2eu4v37jtjmwvev0jyk3zjhxg2wnt56seyez97.webp b/public/images/alumni/avatars/npub1u07xw079lxwv24xslarh2eu4v37jtjmwvev0jyk3zjhxg2wnt56seyez97.webp new file mode 100644 index 00000000..2306d442 Binary files /dev/null and b/public/images/alumni/avatars/npub1u07xw079lxwv24xslarh2eu4v37jtjmwvev0jyk3zjhxg2wnt56seyez97.webp differ diff --git a/public/images/alumni/avatars/npub1u37h8rhgm9f95d90lpk2afw8h4t75kf6w8vmga2zz9jsx3atzpuqlmw8vy.webp b/public/images/alumni/avatars/npub1u37h8rhgm9f95d90lpk2afw8h4t75kf6w8vmga2zz9jsx3atzpuqlmw8vy.webp new file mode 100644 index 00000000..7d987939 Binary files /dev/null and b/public/images/alumni/avatars/npub1u37h8rhgm9f95d90lpk2afw8h4t75kf6w8vmga2zz9jsx3atzpuqlmw8vy.webp differ diff --git a/public/images/alumni/avatars/npub1u8lnhlw5usp3t9vmpz60ejpyt649z33hu82wc2hpv6m5xdqmuxhs46turz.webp b/public/images/alumni/avatars/npub1u8lnhlw5usp3t9vmpz60ejpyt649z33hu82wc2hpv6m5xdqmuxhs46turz.webp new file mode 100644 index 00000000..7893675c Binary files /dev/null and b/public/images/alumni/avatars/npub1u8lnhlw5usp3t9vmpz60ejpyt649z33hu82wc2hpv6m5xdqmuxhs46turz.webp differ diff --git a/public/images/alumni/avatars/npub1uac67zc9er54ln0kl6e4qp2y6ta3enfcg7ywnayshvlw9r5w6ehsqq99rx.webp b/public/images/alumni/avatars/npub1uac67zc9er54ln0kl6e4qp2y6ta3enfcg7ywnayshvlw9r5w6ehsqq99rx.webp new file mode 100644 index 00000000..e2a51f9e Binary files /dev/null and b/public/images/alumni/avatars/npub1uac67zc9er54ln0kl6e4qp2y6ta3enfcg7ywnayshvlw9r5w6ehsqq99rx.webp differ diff --git a/public/images/alumni/avatars/npub1wf4pufsucer5va8g9p0rj5dnhvfeh6d8w0g6eayaep5dhps6rsgs43dgh9.webp b/public/images/alumni/avatars/npub1wf4pufsucer5va8g9p0rj5dnhvfeh6d8w0g6eayaep5dhps6rsgs43dgh9.webp new file mode 100644 index 00000000..b3e695c3 Binary files /dev/null and b/public/images/alumni/avatars/npub1wf4pufsucer5va8g9p0rj5dnhvfeh6d8w0g6eayaep5dhps6rsgs43dgh9.webp differ diff --git a/public/images/alumni/avatars/npub1y3yqdp44vg62ys8anqnjpx6cfprl848ev4lsmx5h4mznyz3xft9sen050h.webp b/public/images/alumni/avatars/npub1y3yqdp44vg62ys8anqnjpx6cfprl848ev4lsmx5h4mznyz3xft9sen050h.webp new file mode 100644 index 00000000..015fa474 Binary files /dev/null and b/public/images/alumni/avatars/npub1y3yqdp44vg62ys8anqnjpx6cfprl848ev4lsmx5h4mznyz3xft9sen050h.webp differ diff --git a/public/images/alumni/avatars/npub1yay8e9sqk94jfgdlkpgeelj2t5ddsj2eu0xwt4kh4xw5ses2rauqnstrdv.webp b/public/images/alumni/avatars/npub1yay8e9sqk94jfgdlkpgeelj2t5ddsj2eu0xwt4kh4xw5ses2rauqnstrdv.webp new file mode 100644 index 00000000..0700760a Binary files /dev/null and b/public/images/alumni/avatars/npub1yay8e9sqk94jfgdlkpgeelj2t5ddsj2eu0xwt4kh4xw5ses2rauqnstrdv.webp differ diff --git a/public/images/alumni/avatars/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr.webp b/public/images/alumni/avatars/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr.webp new file mode 100644 index 00000000..9afb2613 Binary files /dev/null and b/public/images/alumni/avatars/npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr.webp differ diff --git a/public/images/alumni/avatars/npub1zc6ts76lel22d38l9uk7zazsen8yd7dtuzcz5uv8d3vkast9hlks4725sl.webp b/public/images/alumni/avatars/npub1zc6ts76lel22d38l9uk7zazsen8yd7dtuzcz5uv8d3vkast9hlks4725sl.webp new file mode 100644 index 00000000..202b85f8 Binary files /dev/null and b/public/images/alumni/avatars/npub1zc6ts76lel22d38l9uk7zazsen8yd7dtuzcz5uv8d3vkast9hlks4725sl.webp differ diff --git a/public/images/alumni/avatars/npub1zel8lcq6w6mtajwj4xcedvvvwts4p6v9l045dmn9rp570dqry7zsyll0dt.webp b/public/images/alumni/avatars/npub1zel8lcq6w6mtajwj4xcedvvvwts4p6v9l045dmn9rp570dqry7zsyll0dt.webp new file mode 100644 index 00000000..7245f7cb Binary files /dev/null and b/public/images/alumni/avatars/npub1zel8lcq6w6mtajwj4xcedvvvwts4p6v9l045dmn9rp570dqry7zsyll0dt.webp differ diff --git a/public/images/alumni/avatars/npub1zthq85gksjsjthv8h6rec2qeqs2mu0emrm9xknkhgw7hfl7csrnq6wxm56.webp b/public/images/alumni/avatars/npub1zthq85gksjsjthv8h6rec2qeqs2mu0emrm9xknkhgw7hfl7csrnq6wxm56.webp new file mode 100644 index 00000000..2a9b7257 Binary files /dev/null and b/public/images/alumni/avatars/npub1zthq85gksjsjthv8h6rec2qeqs2mu0emrm9xknkhgw7hfl7csrnq6wxm56.webp differ diff --git a/public/images/alumni/avatars/npub1zzt0d0s2f4lsanpd7nkjep5r79p7ljq7aw37eek64hf0ef6v0mxqgwljrv.webp b/public/images/alumni/avatars/npub1zzt0d0s2f4lsanpd7nkjep5r79p7ljq7aw37eek64hf0ef6v0mxqgwljrv.webp new file mode 100644 index 00000000..1c2a28f2 Binary files /dev/null and b/public/images/alumni/avatars/npub1zzt0d0s2f4lsanpd7nkjep5r79p7ljq7aw37eek64hf0ef6v0mxqgwljrv.webp differ diff --git a/scripts/cache-alumni-avatars.mjs b/scripts/cache-alumni-avatars.mjs new file mode 100644 index 00000000..324a268d --- /dev/null +++ b/scripts/cache-alumni-avatars.mjs @@ -0,0 +1,175 @@ +#!/usr/bin/env bun +import { createHash } from 'node:crypto'; +import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import sharp from 'sharp'; + +const ROOT = new URL('..', import.meta.url); +const DATA_PATH = new URL('src/data/sovengAlumni.json', `${ROOT}/`); +const MANIFEST_PATH = new URL('src/data/sovengAlumniAvatarCache.json', `${ROOT}/`); +const OUTPUT_DIR = new URL('public/images/alumni/avatars/', `${ROOT}/`); +const PUBLIC_PREFIX = '/images/alumni/avatars/'; +const AVATAR_SIZE = 128; +const CONCURRENCY = 6; +const MAX_IMAGE_BYTES = 6 * 1024 * 1024; +const REQUEST_TIMEOUT_MS = 20_000; +const USER_AGENT = 'SovereignEngineering.io alumni avatar cache (+https://sovereignengineering.io/alumni)'; +const STRICT = process.argv.includes('--strict'); + +function cleanText(value) { + if (typeof value !== 'string') return undefined; + const cleaned = value.replace(/\s+/g, ' ').trim(); + return cleaned.length > 0 ? cleaned : undefined; +} + +function safeImageUrl(value) { + const imageHref = cleanText(value); + if (!imageHref) return undefined; + + try { + const url = new URL(imageHref); + if (url.protocol !== 'https:') return undefined; + if (url.username || url.password) return undefined; + if (!url.hostname) return undefined; + return url.href; + } catch { + return undefined; + } +} + +function sha256(buffer) { + return createHash('sha256').update(buffer).digest('hex'); +} + +function avatarFileName(profile) { + return `${profile.npub}.webp`; +} + +async function fetchAvatar(source) { + const response = await fetch(source, { + headers: { accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', 'user-agent': USER_AGENT }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const contentLength = Number(response.headers.get('content-length') || 0); + if (contentLength > MAX_IMAGE_BYTES) throw new Error(`too large: ${contentLength} bytes`); + + const input = Buffer.from(await response.arrayBuffer()); + if (input.length > MAX_IMAGE_BYTES) throw new Error(`too large: ${input.length} bytes`); + return input; +} + +async function renderAvatar(input) { + return await sharp(input, { animated: false, failOn: 'none', limitInputPixels: 16_000_000 }) + .rotate() + .resize(AVATAR_SIZE, AVATAR_SIZE, { fit: 'cover' }) + .webp({ effort: 4, quality: 78 }) + .toBuffer(); +} + +async function processProfile(profile) { + const source = safeImageUrl(profile.picture); + if (!source) return { status: 'skipped', profile }; + + const input = await fetchAvatar(source); + const output = await renderAvatar(input); + const filename = avatarFileName(profile); + await writeFile(new URL(filename, OUTPUT_DIR), output); + + return { + status: 'cached', + profile, + entry: { + src: `${PUBLIC_PREFIX}${filename}`, + source, + width: AVATAR_SIZE, + height: AVATAR_SIZE, + bytes: output.length, + sha256: sha256(output), + }, + }; +} + +async function mapConcurrent(items, mapper, concurrency) { + const results = new Array(items.length); + let nextIndex = 0; + + async function worker() { + while (nextIndex < items.length) { + const index = nextIndex; + nextIndex += 1; + try { + results[index] = await mapper(items[index], index); + } catch (error) { + results[index] = { status: 'failed', profile: items[index], error }; + } + } + } + + await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker)); + return results; +} + +async function removeStaleFiles(keep) { + const files = await readdir(OUTPUT_DIR).catch(() => []); + await Promise.all( + files + .filter((file) => file.endsWith('.webp') && !keep.has(file)) + .map((file) => unlink(join(OUTPUT_DIR.pathname, file))), + ); +} + +const profiles = JSON.parse(await readFile(DATA_PATH, 'utf8')); +const profilesWithPictures = profiles.filter((profile) => safeImageUrl(profile.picture)); + +await mkdir(OUTPUT_DIR, { recursive: true }); +const results = await mapConcurrent(profilesWithPictures, processProfile, CONCURRENCY); +const failures = results.filter((result) => result.status === 'failed'); + +for (const failure of failures) { + console.warn(`${failure.profile.npub}: ${failure.error?.message || failure.error}`); +} + +if (STRICT && failures.length > 0) { + throw new Error(`Failed to cache ${failures.length} alumni avatar(s)`); +} + +const avatars = Object.fromEntries( + results + .filter((result) => result.status === 'cached') + .map((result) => [result.profile.npub, result.entry]) + .sort(([left], [right]) => left.localeCompare(right)), +); +const failed = Object.fromEntries( + failures + .map((failure) => [ + failure.profile.npub, + { + source: safeImageUrl(failure.profile.picture), + error: failure.error?.message || String(failure.error), + }, + ]) + .sort(([left], [right]) => left.localeCompare(right)), +); + +await removeStaleFiles(new Set(Object.values(avatars).map((entry) => entry.src.replace(PUBLIC_PREFIX, '')))); +await writeFile( + MANIFEST_PATH, + `${JSON.stringify( + { + generatedAt: new Date().toISOString(), + format: 'webp', + width: AVATAR_SIZE, + height: AVATAR_SIZE, + avatars, + failed, + }, + null, + 2, + )}\n`, +); + +console.log(`Cached ${Object.keys(avatars).length}/${profilesWithPictures.length} alumni avatars to ${OUTPUT_DIR.pathname}`); +if (failures.length > 0) console.log(`Skipped ${failures.length} broken upstream avatar(s); page will show initials for them.`); diff --git a/scripts/extract-alumni-kind0.mjs b/scripts/extract-alumni-kind0.mjs new file mode 100644 index 00000000..f07a2a55 --- /dev/null +++ b/scripts/extract-alumni-kind0.mjs @@ -0,0 +1,244 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { join } from 'node:path'; +import { createHash } from 'node:crypto'; + +const root = process.cwd(); +const alumniPath = join(root, 'src/data/sovengAlumni.json'); +const membershipSourceUrl = 'https://following.space/d/sier9e7ih6k2?p=83d999a148625c3d2bb819af3064c0f6a12d7da88f68b2c69221f3a746171d19'; +const membershipAuthorPubkey = '83d999a148625c3d2bb819af3064c0f6a12d7da88f68b2c69221f3a746171d19'; +const membershipListId = 'sier9e7ih6k2'; +const membershipListKind = 39089; +const followListRelayUrls = [ + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nostr.oxtr.dev', + 'wss://nostr-pub.wellorder.net', + 'wss://nos.lol', + 'wss://relay.primal.net', +]; +const relayUrls = [ + 'wss://nos.lol', + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://nostr.wine', + 'wss://relay.nostr.band', + 'wss://purplepag.es', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr.mom', +]; +const chunkSize = 30; + +function parseMetadata(event) { + try { + const parsed = JSON.parse(event.content); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +function asCleanString(value) { + return typeof value === 'string' && value.trim() ? value.trim() : undefined; +} + +function uniqueByPubkey(records) { + const seen = new Set(); + const result = []; + for (const record of records) { + if (seen.has(record.pubkey)) continue; + seen.add(record.pubkey); + result.push(record); + } + return result; +} + +function isHex64(value) { + return typeof value === 'string' && /^[0-9a-f]{64}$/i.test(value); +} + +function computeNostrEventId(event) { + const serialized = JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]); + return createHash('sha256').update(serialized, 'utf8').digest('hex'); +} + +function isValidKind0Event(event) { + if (!event || event.kind !== 0 || !isHex64(event.pubkey) || !isHex64(event.id)) return false; + if (!Number.isInteger(event.created_at) || event.created_at <= 0) return false; + if (!Array.isArray(event.tags) || event.tags.some((tag) => !Array.isArray(tag) || tag.some((item) => typeof item !== 'string'))) return false; + if (typeof event.content !== 'string') return false; + return computeNostrEventId(event) === event.id; +} + +function rememberEvent(eventsByPubkey, event, allowedPubkeys) { + if (!isValidKind0Event(event)) return; + if (allowedPubkeys && !allowedPubkeys.has(event.pubkey)) return; + const existing = eventsByPubkey.get(event.pubkey); + if (!existing || event.created_at > existing.created_at || (event.created_at === existing.created_at && event.id > existing.id)) { + eventsByPubkey.set(event.pubkey, event); + } +} + +function parseEventLines(stdout, eventsByPubkey, allowedPubkeys) { + for (const line of stdout.split('\n')) { + const trimmed = line.trim(); + if (!trimmed.startsWith('{')) continue; + try { + rememberEvent(eventsByPubkey, JSON.parse(trimmed), allowedPubkeys); + } catch { + // Ignore relay notices and partial lines. + } + } +} + +function parseJsonEventLines(stdout) { + const events = []; + for (const line of stdout.split('\n')) { + const trimmed = line.trim(); + if (!trimmed.startsWith('{')) continue; + try { + events.push(JSON.parse(trimmed)); + } catch { + // Ignore relay notices and partial lines. + } + } + return events; +} + +function isValidMembershipListEvent(event) { + if (!event || event.kind !== membershipListKind || event.pubkey !== membershipAuthorPubkey || !isHex64(event.id)) return false; + if (!Number.isInteger(event.created_at) || event.created_at <= 0) return false; + if (!Array.isArray(event.tags) || event.tags.some((tag) => !Array.isArray(tag) || tag.some((item) => typeof item !== 'string'))) return false; + if (typeof event.content !== 'string') return false; + if (computeNostrEventId(event) !== event.id) return false; + return event.tags.some((tag) => tag[0] === 'd' && tag[1] === membershipListId); +} + +function hexToNpub(pubkey, existingByPubkey) { + const existing = existingByPubkey.get(pubkey); + if (existing?.npub) return existing.npub; + const result = spawnSync('nak', ['encode', 'npub', pubkey], { cwd: root, encoding: 'utf8', maxBuffer: 1024 * 1024 }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(`nak encode npub exited with ${result.status}: ${result.stderr}`); + const npub = result.stdout.trim(); + if (!/^npub1[023456789acdefghjklmnpqrstuvwxyz]+$/.test(npub)) throw new Error(`nak returned invalid npub for ${pubkey}`); + return npub; +} + +function fetchMembershipSeedRecords(existingRecords) { + const existingByPubkey = new Map(existingRecords.map((record) => [record.pubkey, record])); + const args = ['req', '-k', String(membershipListKind), '-a', membershipAuthorPubkey, '-d', membershipListId, '-l', '20', ...followListRelayUrls]; + console.error(`fetching source follow-list ${membershipListKind}:${membershipAuthorPubkey}:${membershipListId}`); + const result = spawnSync('nak', args, { cwd: root, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024, timeout: 90_000 }); + if (result.error) throw result.error; + if (result.status !== 0) throw new Error(`nak follow-list req exited with ${result.status}: ${result.stderr}`); + + const events = parseJsonEventLines(result.stdout).filter(isValidMembershipListEvent); + if (events.length === 0) throw new Error('no valid source follow-list event returned from relays'); + events.sort((left, right) => right.created_at - left.created_at || right.id.localeCompare(left.id)); + + const event = events[0]; + const seen = new Set(); + const seedRecords = []; + for (const tag of event.tags) { + if (tag[0] !== 'p' || !isHex64(tag[1]) || seen.has(tag[1])) continue; + seen.add(tag[1]); + seedRecords.push({ pubkey: tag[1], npub: hexToNpub(tag[1], existingByPubkey) }); + } + if (seedRecords.length === 0) throw new Error('source follow-list event has no p tags'); + console.error(`source follow-list event ${event.id} has ${seedRecords.length} unique pubkeys`); + return seedRecords; +} + +function seedExistingKind0Events(eventsByPubkey, existingRecords, allowedPubkeys) { + for (const record of existingRecords) rememberEvent(eventsByPubkey, record.kind0, allowedPubkeys); +} + +function fetchKind0Events(seedRecords, existingRecords) { + const pubkeys = seedRecords.map((record) => record.pubkey); + const allowedPubkeys = new Set(pubkeys); + const eventsByPubkey = new Map(); + seedExistingKind0Events(eventsByPubkey, existingRecords, allowedPubkeys); + for (let index = 0; index < pubkeys.length; index += chunkSize) { + const chunk = pubkeys.slice(index, index + chunkSize); + const chunkSet = new Set(chunk); + const args = ['req', '-k', '0', '-l', String(chunk.length * relayUrls.length * 3)]; + for (const pubkey of chunk) args.push('-a', pubkey); + args.push(...relayUrls); + + console.error(`fetching kind0 chunk ${index / chunkSize + 1}/${Math.ceil(pubkeys.length / chunkSize)} (${chunk.length} pubkeys)`); + const result = spawnSync('nak', args, { cwd: root, encoding: 'utf8', maxBuffer: 50 * 1024 * 1024 }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`nak exited with ${result.status}: ${result.stderr}`); + } + parseEventLines(result.stdout, eventsByPubkey, chunkSet); + } + + const missingAfterBatch = seedRecords.filter((record) => !eventsByPubkey.has(record.pubkey)); + for (const record of missingAfterBatch) { + console.error(`fallback kind0 fetch for ${record.npub}`); + const reqArgs = ['req', '-k', '0', '-l', '20', '-a', record.pubkey, ...relayUrls]; + const reqResult = spawnSync('nak', reqArgs, { cwd: root, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }); + if (!reqResult.error && reqResult.status === 0) { + parseEventLines(reqResult.stdout, eventsByPubkey, allowedPubkeys); + } + if (eventsByPubkey.has(record.pubkey)) continue; + + const fetchResult = spawnSync('nak', ['fetch', record.npub], { cwd: root, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }); + if (!fetchResult.error && fetchResult.status === 0) { + parseEventLines(fetchResult.stdout, eventsByPubkey, allowedPubkeys); + } + } + + return eventsByPubkey; +} + +const existingRecords = uniqueByPubkey(JSON.parse(readFileSync(alumniPath, 'utf8'))); +const seedRecords = fetchMembershipSeedRecords(existingRecords); +const pubkeys = seedRecords.map((record) => record.pubkey); +const fetchedAt = new Date().toISOString(); +const eventsByPubkey = fetchKind0Events(seedRecords, existingRecords); + +const missing = pubkeys.filter((pubkey) => !eventsByPubkey.has(pubkey)); +if (missing.length > 0) { + console.error(`missing kind0 events for ${missing.length}/${pubkeys.length} pubkeys:`); + for (const pubkey of missing) console.error(`- ${pubkey}`); + process.exit(1); +} + +const records = seedRecords + .map((seed) => { + const kind0 = eventsByPubkey.get(seed.pubkey); + const metadata = parseMetadata(kind0); + const name = asCleanString(metadata.name); + const displayName = asCleanString(metadata.display_name) ?? asCleanString(metadata.displayName); + const about = asCleanString(metadata.about); + const picture = asCleanString(metadata.picture); + const nip05 = asCleanString(metadata.nip05); + + return { + pubkey: seed.pubkey, + npub: seed.npub, + ...(name ? { name } : {}), + ...(displayName ? { displayName } : {}), + ...(about ? { about } : {}), + ...(nip05 ? { nip05 } : {}), + ...(picture ? { picture } : {}), + kind0, + source: { + membershipSourceUrl, + relayUrls, + fetchedAt, + }, + }; + }) + .sort((left, right) => { + const leftName = left.displayName || left.name || left.npub; + const rightName = right.displayName || right.name || right.npub; + return leftName.localeCompare(rightName, undefined, { sensitivity: 'base' }) || left.npub.localeCompare(right.npub); + }); + +writeFileSync(alumniPath, `${JSON.stringify(records, null, 2)}\n`); +console.log(`wrote ${records.length} source-locked alumni records with raw kind0 events to ${alumniPath}`); diff --git a/scripts/test-alumni-page-source.mjs b/scripts/test-alumni-page-source.mjs new file mode 100644 index 00000000..9d47e810 --- /dev/null +++ b/scripts/test-alumni-page-source.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env bun +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; + +const pagePath = 'src/pages/alumni.astro'; +assert.equal(existsSync(pagePath), true, '/alumni route must exist'); +assert.equal(existsSync('public/images/copy-to-clipboard.svg'), false, 'unused copy icon asset should be removed'); + +const page = readFileSync(pagePath, 'utf8'); +assert.match(page, /\s*Social Graph\s*]*id="alumni-title"[\s\S]*Alumni[\s\S]*<\/h1>/, 'hero title should be Alumni'); +assert.match( + page, + /Builders, designers, and founders from Sovereign Engineering working on a freer internet with Bitcoin, Nostr, and open protocols\./, + 'hero lede should use the tightened alumni copy' +); +assert.match( + page, + /class="[^"]*alumni-source-link[^"]*"[\s\S]*Nostr follow-list[\s\S]*fa-arrow-up-right-from-square/, + 'source link should sit under hero copy with the shared external-link icon' +); +assert.match(page, /getAlumniProfileViewModel/, 'alumni route should render safe profile view models'); +assert.match(page, /class="[^"]*alumni-grid/, 'alumni route should render the profile grid'); +assert.match(page, /class="[^"]*alumni-card/, 'alumni route should render profile cards'); +const alumniGridRule = page.match(/\.alumni-grid\s*\{[\s\S]*?\n \}/)?.[0] ?? ''; +assert.match(alumniGridRule, /gap:\s*1rem/, 'grid should use breathing room between cards'); +assert.doesNotMatch(alumniGridRule, /margin-top:/, 'grid should not need extra offset hacks'); +assert.match(page, /\.alumni-card\s*\{[\s\S]*border:\s*1px solid rgb\(255 255 255 \/ 9%\)/, 'cards should use a simple subtle border'); +assert.match(page, /\.alumni-card:hover\s*\{[\s\S]*border-color:\s*rgb\(255 255 255 \/ 18%\)/, 'card hover should stay restrained'); +assert.doesNotMatch(page, /\.alumni-card::before|\.alumni-card::after/, 'card sparkle and separator overlays should be removed'); +assert.match(page, /class="[^"]*alumni-hero bg-black pt-16 pb-6 text-white sm:pt-20 sm:pb-8/, 'hero should keep the simplified spacing'); +assert.match(page, /\.alumni-hero\s*\{[\s\S]*min-height:\s*0/, 'hero should not force tall viewport spacing'); +assert.match(page, /class="[^"]*alumni-directory bg-black pt-4 pb-16 text-white sm:pt-4 sm:pb-20/, 'directory should sit tighter under the hero'); +assert.match( + page, + /class="[^"]*alumni-card-top[\s\S]*alumni-avatar-link[\s\S]*alumni-card-identity[\s\S]*alumni-card-title/, + 'card name should sit next to profile image' +); +assert.match(page, /class="[^"]*alumni-card-identity[\s\S]*alumni-card-title[\s\S]*alumni-handle/, 'handle should sit under the card name'); +assert.match( + page, + /class="[^"]*alumni-card-title[^"]*"[\s\S]*fa-arrow-up-right-from-square/, + 'card title link should use the smaller FontAwesome external-link icon' +); +assert.match(page, /class="[^"]*alumni-handle[^"]*"[\s\S]*\{profile\.handle\}/, 'card should show the compact handle line'); +assert.doesNotMatch(page, /\{profile\.npub\}<\/span>/, 'full npub text should no longer render in the card body'); +assert.doesNotMatch(page, /alumni-npub-copy|data-alumni-copy|Copy npub|copy-to-clipboard\.svg/, 'copy npub UI and wiring should be removed'); +assert.doesNotMatch(page, /navigator\.clipboard\.writeText\(value\)|is-copied|is-copy-failed/, 'clipboard logic should be removed'); +assert.match(page, /data-alumni-avatar-image/, 'avatar images should have an error fallback hook'); +assert.match(page, /width="64"[\s\S]*height="64"/, 'avatar images should declare dimensions to reduce layout shift'); +assert.match(page, /loading=\{index < 6 \? 'eager' : 'lazy'\}/, 'above-fold avatars should load eagerly while offscreen avatars stay lazy'); +assert.match(page, /fetchpriority=\{index < 3 \? 'high' : 'auto'\}/, 'first visible avatars should get higher fetch priority'); +assert.match(page, /contain:\s*layout paint/, 'cards should contain layout/paint without hiding visible content'); +assert.match(page, /referrerpolicy="no-referrer"/, 'external profile images should avoid leaking referrers'); +assert.doesNotMatch(page, /