@@ -12,7 +12,7 @@ require "json"
1212require " uri"
1313require " yaml"
1414
15- DEBUG = ARGV .empty? || ! [" sync" , " build" , " deploy" , " health" ].includes?(ARGV [0 ])
15+ DEBUG = ARGV .empty? || ! [" sync" , " sync-nostr " , " build" , " deploy" , " health" ].includes?(ARGV [0 ])
1616TRACE = ENV [" TRACE" ]? == " 1"
1717
1818WILDCARD_HOST = " 0.0.0.0"
@@ -74,6 +74,9 @@ def main
7474 elsif ARGV .size == 1 && ARGV [0 ] == " sync"
7575 check
7676 sync
77+ elsif ARGV .size >= 1 && ARGV [0 ] == " sync-nostr"
78+ profiles = ARGV .size > 1 && ARGV [1 ] == " profiles"
79+ sync_nostr(config, profiles: profiles, output_relays: [" ws://localhost:7777" , " wss://nostr.codonaft.com" , " wss://purplepag.es" , " wss://nostr.oxtr.dev" , " wss://nostr.girino.org" ])
7780 elsif ARGV .size == 1 && ARGV [0 ] == " health"
7881 check
7982 health(config)
@@ -126,6 +129,10 @@ def usage
126129 download newest non-conflicting versioned files to _ohmyvps and _hosts
127130 upload newest files from _ohmyvps, _hosts and .build
128131
132+ #{ script } sync-nostr [profile]
133+ fetch nostr events, push them to my relays
134+ don't send events to profile-specific relays by default
135+
129136 #{ script } health
130137 run health checks
131138
@@ -392,6 +399,13 @@ def health(config)
392399 end
393400 step(" health" )
394401
402+ all_hosts().each { |host |
403+ step(host)
404+ ssh(host, [" sudo /etc/init.d/nginx checkconfig" , " sudo hdparm -t /dev/sd[a-z] | sed \" s!.*= !!\" " ], tty: true )
405+ }
406+
407+ warn(" github commit hash is different\n " ) unless JSON .parse(HTTP ::Client .get(" https://api.github.com/repos/codonaft/codonaft.github.io/commits" ).body)[0 ][" sha" ].as_s == ` git rev-parse HEAD` .strip
408+
395409 banlists =
396410 if nonempty_exists?(BANLISTS ) && File .info(BANLISTS ).modification_time + 1 .days > Time .utc
397411 File .read_lines(BANLISTS ).to_set
@@ -461,7 +475,7 @@ def health(config)
461475 .map { |i | i.strip }
462476 .reject { |i | i.empty? }
463477 .map { |i | URI .parse(i) }
464- .reject { |i | i.hostname.nil? || i.hostname == " 127.0.0.1 " } + relay_mirrors
478+ .reject { |i | i.hostname.nil? || i.hostname == " localhost " } + relay_mirrors
465479 wss_uris = (trackers + other_relays + ` git grep --only-matching 'wss://[a-zA-Z0-9/._-]*'`
466480 .split('\n' )
467481 .map { |i | i.strip.gsub(/.*wss:\/\/ / , " " ) }
@@ -714,7 +728,6 @@ def check
714728 step(" check" )
715729
716730 ps = processes()
717-
718731 current_ps_name = " /#{ Path [__FILE__ ].basename} "
719732 raise " other instance keeps running" if ps
720733 .select { |(_ , i )| i.includes?(" crystal" ) && i.includes?(current_ps_name) }
@@ -723,6 +736,8 @@ def check
723736 puts(" checking dependencies" )
724737 system(" which bsdtar bundle css-minify node pnpm podman rsync scour svgcleaner svgo uglifyjs wget >>/dev/null" )
725738 raise " missing deps, run: cd && npm install 'css-minify@2.0.0' 'svgo@3.3.2' && USE='lz4 xxhash zstd' emerge '=app-arch/libarchive-3.7.9' '=app-arch/unzip-6.0_p27-r1' '=app-containers/podman-5.3.2' '=dev-ruby/bundler-2.4.22' '=dev-lang/crystal-1.16.1' '=dev-util/uglifyjs-3.16.1' '=media-gfx/scour-0.38.2-r1' '=media-sound/opus-tools-0.2-r1' '=media-libs/libwebp-1.4.0' '=media-video/mediainfo-24.11' '=net-dns/bind-tools-9.18.0-r1' '=net-libs/nodejs-22.13.1' '=net-misc/rsync-3.4.1' '=net-misc/wget-1.25.0' '=sys-apps/pnpm-bin-9.6.0' && cargo install --locked 'svgcleaner@0.9.5' 'websocat@1.13.0'" unless $? .success?
739+ system(" podman ps >>/dev/null" )
740+ raise " podman failed" unless $? .success?
726741
727742 system(<<-STRING
728743 set -euo pipefail
841856def build_jekyll (host : String , * , configs : Array (Path ))
842857 prepare_jekyll
843858
859+ upstream_url = configs.map { |i | YAML .parse(File .read(i))[" theme_settings" ][" upstream_url" ]? }.select { |i | ! i.nil? }[0 ]?
844860 url = URI .parse(configs.reverse.map { |i | YAML .parse(File .read(i))[" url" ] }.select { |i | ! i.nil? }[0 ].as_s)
845861 destination = Path [" /var/www" ].join(url.hostname.not_nil!)
846862 output_dir = BUILD_DIR .join(host, destination)
@@ -853,11 +869,15 @@ def build_jekyll(host : String, *, configs : Array(Path))
853869 configs_arg = configs.empty? ? " " : " --config #{ configs.join(',' ) } "
854870 system(" bundle exec jekyll build --future --destination #{ output_dir } #{ configs_arg } " )
855871 raise " jekyll build failure" unless $? .success?
872+ unless upstream_url.nil?
873+ system(" sed --in-place 's!#{ upstream_url.as_s.gsub('.' , " \\ ." ) } !#{ url } !g' #{ output_dir.join(" sitemap.xml" ) } " )
874+ raise " failed to update sitemap" unless $? .success?
875+ end
856876end
857877
858878def prepare_jekyll
859879 unless File .directory?(" vendor" )
860- FileUtils .rm_r ([" Gemfile.lock" , " .bundle" ])
880+ FileUtils .rm_rf ([" Gemfile.lock" , " .bundle" ])
861881 system(<<-STRING
862882 set -xeuo pipefail
863883 gem update bundler
@@ -1120,6 +1140,117 @@ def check_manual_upload(host : String, *, owner : String, group : String, mode :
11201140 end
11211141end
11221142
1143+ def sync_nostr (config, * , profiles : Bool , output_relays : Array (String ))
1144+ # TODO: do the same thing as cron bash job that uses rust nostr cli client? connect to relays over tor?
1145+
1146+ limit = 100
1147+ now = Time .utc
1148+ from = now - 3 .years # TODO: last commit date? last blog date? previous blog date?
1149+ to = now + 15 .minutes
1150+ profile_kinds = [0 , 3 , 10002 ]
1151+
1152+ nostr_config = config[" theme_settings" ][" nostr" ]
1153+ npub = nostr_config[" npub" ].as_s
1154+ pk = JSON .parse(` nak decode #{ npub } ` )[" pubkey" ].as_s
1155+
1156+ profile_relays = nostr_config[" profile_relays" ].as_a.map { |i | i.as_s }
1157+ relays = nostr_config[" relays" ].as_a.map { |i | i.as_s }
1158+ read_cache_relays = nostr_config[" read_cache_relays" ].as_a.map { |i | i.as_s }
1159+ input_relays : Array (String ) = relays + read_cache_relays + profile_relays
1160+
1161+ step(" sync-nostr" )
1162+ trace(" pk=#{ pk } input_relays=#{ input_relays } output_relays=#{ output_relays } " )
1163+
1164+ nak = " nak req --limit #{ limit } --since #{ from.to_unix } --until #{ to.to_unix } "
1165+
1166+ mentions = parse_nostr_events(` #{ nak } -p #{ pk } #{ input_relays.join(' ' ) } ` )
1167+ .reject { |i |
1168+ return false unless i[" tags" ].as_a.any? { |t | t.size > 1 && t[0 ] == " p" && t[1 ] == pk }
1169+ k = i[" kind" ].as_i
1170+ [3 , 1984 , 4454 ].includes?(k) || (k >= 5000 && k <= 7000 ) || (k >= 10000 && k <= 10102 ) || (k >= 20000 && k < 30000 ) || (k >= 30000 && k <= 30267 )
1171+ }
1172+ puts(" fetched #{ mentions.size } mentions" )
1173+
1174+ parsed_authored_events = parse_nostr_events(` #{ nak } -a #{ pk } #{ input_relays.join(' ' ) } ` )
1175+ authored_events = parsed_authored_events
1176+ .reject { |i |
1177+ return false unless i[" pubkey" ].as_s == pk
1178+ k = i[" kind" ].as_i
1179+ [4454 , 10044 ].includes?(k) || (k >= 20000 && k < 30000 )
1180+ }
1181+ puts(" fetched #{ authored_events.size } authored events" )
1182+
1183+ puts(" writing events" )
1184+ nak_raw([" event" ] + output_relays, (mentions + authored_events).map { |i | i.to_json })
1185+
1186+ if profiles
1187+ # TODO: check spam
1188+ fetch_comments_command = ([nak, " -k" , " 1" , " -p" , pk] + input_relays).join(' ' )
1189+ trace(fetch_comments_command)
1190+ comments_pks = parse_nostr_events(` #{ fetch_comments_command } ` )
1191+ .select { |i | i[" kind" ].as_i == 1 && i[" pubkey" ].as_s != pk }
1192+ .map { |i | i[" pubkey" ].as_s }.to_set
1193+ request_profiles_command = (
1194+ [nak, " --since" , " 0" , " -k" , " 0" ] + comments_pks.map { |i | " -a #{ i } " } + input_relays
1195+ ).join(' ' )
1196+ trace(request_profiles_command)
1197+ comments_profile_events = parse_nostr_events(` #{ request_profiles_command } ` )
1198+ .select { |i | i[" kind" ].as_i == 0 }
1199+ .group_by { |i | i[" pubkey" ].as_s }
1200+ .reject { |p , _ | p == pk }
1201+ .map { |_ , g | g.max_by { |i | i[" created_at" ].as_i } }
1202+ puts(" profile events of commenters = #{ comments_profile_events.size } " )
1203+
1204+ request_author_profile_events_command = (
1205+ [nak, " --since" , " 0" , " -a" , pk] + profile_kinds.map { |k | " -k #{ k } " } + input_relays
1206+ ).join(' ' )
1207+ trace(request_author_profile_events_command)
1208+ author_profile_events = parse_nostr_events(` #{ request_author_profile_events_command } ` )
1209+ .select { |i | profile_kinds.includes?(i[" kind" ].as_i) }
1210+ puts(" author profile events = #{ author_profile_events.size } " )
1211+ nak_raw([" event" ] + profile_relays, (comments_profile_events + author_profile_events).map { |i | i.to_json })
1212+ end
1213+ puts(" finished writing events" )
1214+ end
1215+
1216+ def nak_raw (args, input : Array (String ) = [] of String )
1217+ # TODO: exit after timeout
1218+ output = Channel (Tuple (String , Process ::Status )).new
1219+ trace(" running nak #{ args } " )
1220+ spawn do
1221+ value = Process .run(command: " nak" , args: args) do |p |
1222+ input.each do |line |
1223+ trace(" writing stdin" )
1224+ p.input.puts(line)
1225+ trace(" writing stdin ok" )
1226+ end
1227+ trace(" closing stdin" )
1228+ p.input.close
1229+ trace(" closed stdin" )
1230+ trace(" reading stdout" )
1231+ result = p.output.gets_to_end
1232+ trace(" finished reading stdout" )
1233+ result
1234+ end
1235+ trace(" sending stdout" )
1236+ output.send({value.strip, $? })
1237+ trace(" sent stdout" )
1238+ end
1239+ trace(" waiting for stdout" )
1240+ value, status = output.receive
1241+ trace(" received stdout" )
1242+ raise " #{ args } : unexpected exit code #{ status } , value=#{ value } " unless status.success?
1243+ value
1244+ end
1245+
1246+ def parse_nostr_events (text )
1247+ text
1248+ .split('\n' )
1249+ .reject { |i | i.strip.empty? }
1250+ .to_set
1251+ .map { |i | JSON .parse(i) }
1252+ end
1253+
11231254def processes
11241255 Dir .entries(" /proc" )
11251256 .select { |entry | entry =~ /^\d +$/ }
@@ -1153,6 +1284,7 @@ def children_recursive_inner(dir : Path) : Array(Path)
11531284end
11541285
11551286def all_hosts : Array (String )
1287+ # TODO: from args
11561288 Dir .children(HOSTS_DIR )
11571289end
11581290
0 commit comments