@@ -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
@@ -1120,6 +1135,117 @@ def check_manual_upload(host : String, *, owner : String, group : String, mode :
11201135 end
11211136end
11221137
1138+ def sync_nostr (config, * , profiles : Bool , output_relays : Array (String ))
1139+ # TODO: do the same thing as cron bash job that uses rust nostr cli client? connect to relays over tor?
1140+
1141+ limit = 100
1142+ now = Time .utc
1143+ from = now - 3 .years # TODO: last commit date? last blog date? previous blog date?
1144+ to = now + 15 .minutes
1145+ profile_kinds = [0 , 3 , 10002 ]
1146+
1147+ nostr_config = config[" theme_settings" ][" nostr" ]
1148+ npub = nostr_config[" npub" ].as_s
1149+ pk = JSON .parse(` nak decode #{ npub } ` )[" pubkey" ].as_s
1150+
1151+ profile_relays = nostr_config[" profile_relays" ].as_a.map { |i | i.as_s }
1152+ relays = nostr_config[" relays" ].as_a.map { |i | i.as_s }
1153+ read_cache_relays = nostr_config[" read_cache_relays" ].as_a.map { |i | i.as_s }
1154+ input_relays : Array (String ) = relays + read_cache_relays + profile_relays
1155+
1156+ step(" sync-nostr" )
1157+ trace(" pk=#{ pk } input_relays=#{ input_relays } output_relays=#{ output_relays } " )
1158+
1159+ nak = " nak req --limit #{ limit } --since #{ from.to_unix } --until #{ to.to_unix } "
1160+
1161+ mentions = parse_nostr_events(` #{ nak } -p #{ pk } #{ input_relays.join(' ' ) } ` )
1162+ .reject { |i |
1163+ return false unless i[" tags" ].as_a.any? { |t | t.size > 1 && t[0 ] == " p" && t[1 ] == pk }
1164+ k = i[" kind" ].as_i
1165+ [3 , 1984 , 4454 ].includes?(k) || (k >= 5000 && k <= 7000 ) || (k >= 10000 && k <= 10102 ) || (k >= 20000 && k < 30000 ) || (k >= 30000 && k <= 30267 )
1166+ }
1167+ puts(" fetched #{ mentions.size } mentions" )
1168+
1169+ parsed_authored_events = parse_nostr_events(` #{ nak } -a #{ pk } #{ input_relays.join(' ' ) } ` )
1170+ authored_events = parsed_authored_events
1171+ .reject { |i |
1172+ return false unless i[" pubkey" ].as_s == pk
1173+ k = i[" kind" ].as_i
1174+ [4454 , 10044 ].includes?(k) || (k >= 20000 && k < 30000 )
1175+ }
1176+ puts(" fetched #{ authored_events.size } authored events" )
1177+
1178+ puts(" writing events" )
1179+ nak_raw([" event" ] + output_relays, (mentions + authored_events).map { |i | i.to_json })
1180+
1181+ if profiles
1182+ # TODO: check spam
1183+ fetch_comments_command = ([nak, " -k" , " 1" , " -p" , pk] + input_relays).join(' ' )
1184+ trace(fetch_comments_command)
1185+ comments_pks = parse_nostr_events(` #{ fetch_comments_command } ` )
1186+ .select { |i | i[" kind" ].as_i == 1 && i[" pubkey" ].as_s != pk }
1187+ .map { |i | i[" pubkey" ].as_s }.to_set
1188+ request_profiles_command = (
1189+ [nak, " --since" , " 0" , " -k" , " 0" ] + comments_pks.map { |i | " -a #{ i } " } + input_relays
1190+ ).join(' ' )
1191+ trace(request_profiles_command)
1192+ comments_profile_events = parse_nostr_events(` #{ request_profiles_command } ` )
1193+ .select { |i | i[" kind" ].as_i == 0 }
1194+ .group_by { |i | i[" pubkey" ].as_s }
1195+ .reject { |p , _ | p == pk }
1196+ .map { |_ , g | g.max_by { |i | i[" created_at" ].as_i } }
1197+ puts(" profile events of commenters = #{ comments_profile_events.size } " )
1198+
1199+ request_author_profile_events_command = (
1200+ [nak, " --since" , " 0" , " -a" , pk] + profile_kinds.map { |k | " -k #{ k } " } + input_relays
1201+ ).join(' ' )
1202+ trace(request_author_profile_events_command)
1203+ author_profile_events = parse_nostr_events(` #{ request_author_profile_events_command } ` )
1204+ .select { |i | profile_kinds.includes?(i[" kind" ].as_i) }
1205+ puts(" author profile events = #{ author_profile_events.size } " )
1206+ nak_raw([" event" ] + profile_relays, (comments_profile_events + author_profile_events).map { |i | i.to_json })
1207+ end
1208+ puts(" finished writing events" )
1209+ end
1210+
1211+ def nak_raw (args, input : Array (String ) = [] of String )
1212+ # TODO: exit after timeout
1213+ output = Channel (Tuple (String , Process ::Status )).new
1214+ trace(" running nak #{ args } " )
1215+ spawn do
1216+ value = Process .run(command: " nak" , args: args) do |p |
1217+ input.each do |line |
1218+ trace(" writing stdin" )
1219+ p.input.puts(line)
1220+ trace(" writing stdin ok" )
1221+ end
1222+ trace(" closing stdin" )
1223+ p.input.close
1224+ trace(" closed stdin" )
1225+ trace(" reading stdout" )
1226+ result = p.output.gets_to_end
1227+ trace(" finished reading stdout" )
1228+ result
1229+ end
1230+ trace(" sending stdout" )
1231+ output.send({value.strip, $? })
1232+ trace(" sent stdout" )
1233+ end
1234+ trace(" waiting for stdout" )
1235+ value, status = output.receive
1236+ trace(" received stdout" )
1237+ raise " #{ args } : unexpected exit code #{ status } , value=#{ value } " unless status.success?
1238+ value
1239+ end
1240+
1241+ def parse_nostr_events (text )
1242+ text
1243+ .split('\n' )
1244+ .reject { |i | i.strip.empty? }
1245+ .to_set
1246+ .map { |i | JSON .parse(i) }
1247+ end
1248+
11231249def processes
11241250 Dir .entries(" /proc" )
11251251 .select { |entry | entry =~ /^\d +$/ }
@@ -1153,6 +1279,7 @@ def children_recursive_inner(dir : Path) : Array(Path)
11531279end
11541280
11551281def all_hosts : Array (String )
1282+ # TODO: from args
11561283 Dir .children(HOSTS_DIR )
11571284end
11581285
0 commit comments