@@ -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,10 @@ def health(config)
392399 end
393400 step(" health" )
394401
402+ all_hosts().each { |host | ssh(host, [" sudo /etc/init.d/nginx checkconfig" ], tty: true ) }
403+
404+ 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
405+
395406 banlists =
396407 if nonempty_exists?(BANLISTS ) && File .info(BANLISTS ).modification_time + 1 .days > Time .utc
397408 File .read_lines(BANLISTS ).to_set
@@ -461,7 +472,7 @@ def health(config)
461472 .map { |i | i.strip }
462473 .reject { |i | i.empty? }
463474 .map { |i | URI .parse(i) }
464- .reject { |i | i.hostname.nil? || i.hostname == " 127.0.0.1 " } + relay_mirrors
475+ .reject { |i | i.hostname.nil? || i.hostname == " localhost " } + relay_mirrors
465476 wss_uris = (trackers + other_relays + ` git grep --only-matching 'wss://[a-zA-Z0-9/._-]*'`
466477 .split('\n' )
467478 .map { |i | i.strip.gsub(/.*wss:\/\/ / , " " ) }
@@ -714,7 +725,6 @@ def check
714725 step(" check" )
715726
716727 ps = processes()
717-
718728 current_ps_name = " /#{ Path [__FILE__ ].basename} "
719729 raise " other instance keeps running" if ps
720730 .select { |(_ , i )| i.includes?(" crystal" ) && i.includes?(current_ps_name) }
@@ -723,6 +733,8 @@ def check
723733 puts(" checking dependencies" )
724734 system(" which bsdtar bundle css-minify node pnpm podman rsync scour svgcleaner svgo uglifyjs wget >>/dev/null" )
725735 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?
736+ system(" podman ps >>/dev/null" )
737+ raise " podman failed" unless $? .success?
726738
727739 system(<<-STRING
728740 set -euo pipefail
@@ -1120,6 +1132,117 @@ def check_manual_upload(host : String, *, owner : String, group : String, mode :
11201132 end
11211133end
11221134
1135+ def sync_nostr (config, * , profiles : Bool , output_relays : Array (String ))
1136+ # TODO: do the same thing as cron bash job that uses rust nostr cli client? connect to relays over tor?
1137+
1138+ limit = 100
1139+ now = Time .utc
1140+ from = now - 3 .years # TODO: last commit date? last blog date? previous blog date?
1141+ to = now + 15 .minutes
1142+ profile_kinds = [0 , 3 , 10002 ]
1143+
1144+ nostr_config = config[" theme_settings" ][" nostr" ]
1145+ npub = nostr_config[" npub" ].as_s
1146+ pk = JSON .parse(` nak decode #{ npub } ` )[" pubkey" ].as_s
1147+
1148+ profile_relays = nostr_config[" profile_relays" ].as_a.map { |i | i.as_s }
1149+ relays = nostr_config[" relays" ].as_a.map { |i | i.as_s }
1150+ read_cache_relays = nostr_config[" read_cache_relays" ].as_a.map { |i | i.as_s }
1151+ input_relays : Array (String ) = relays + read_cache_relays + profile_relays
1152+
1153+ step(" sync-nostr" )
1154+ trace(" pk=#{ pk } input_relays=#{ input_relays } output_relays=#{ output_relays } " )
1155+
1156+ nak = " nak req --limit #{ limit } --since #{ from.to_unix } --until #{ to.to_unix } "
1157+
1158+ mentions = parse_nostr_events(` #{ nak } -p #{ pk } #{ input_relays.join(' ' ) } ` )
1159+ .reject { |i |
1160+ return false unless i[" tags" ].as_a.any? { |t | t.size > 1 && t[0 ] == " p" && t[1 ] == pk }
1161+ k = i[" kind" ].as_i
1162+ [3 , 1984 , 4454 ].includes?(k) || (k >= 5000 && k <= 7000 ) || (k >= 10000 && k <= 10102 ) || (k >= 20000 && k < 30000 ) || (k >= 30000 && k <= 30267 )
1163+ }
1164+ puts(" fetched #{ mentions.size } mentions" )
1165+
1166+ parsed_authored_events = parse_nostr_events(` #{ nak } -a #{ pk } #{ input_relays.join(' ' ) } ` )
1167+ authored_events = parsed_authored_events
1168+ .reject { |i |
1169+ return false unless i[" pubkey" ].as_s == pk
1170+ k = i[" kind" ].as_i
1171+ [4454 , 10044 ].includes?(k) || (k >= 20000 && k < 30000 )
1172+ }
1173+ puts(" fetched #{ authored_events.size } authored events" )
1174+
1175+ puts(" writing events" )
1176+ nak_raw([" event" ] + output_relays, (mentions + authored_events).map { |i | i.to_json })
1177+
1178+ if profiles
1179+ # TODO: check spam
1180+ fetch_comments_command = ([nak, " -k" , " 1" , " -p" , pk] + input_relays).join(' ' )
1181+ trace(fetch_comments_command)
1182+ comments_pks = parse_nostr_events(` #{ fetch_comments_command } ` )
1183+ .select { |i | i[" kind" ].as_i == 1 && i[" pubkey" ].as_s != pk }
1184+ .map { |i | i[" pubkey" ].as_s }.to_set
1185+ request_profiles_command = (
1186+ [nak, " --since" , " 0" , " -k" , " 0" ] + comments_pks.map { |i | " -a #{ i } " } + input_relays
1187+ ).join(' ' )
1188+ trace(request_profiles_command)
1189+ comments_profile_events = parse_nostr_events(` #{ request_profiles_command } ` )
1190+ .select { |i | i[" kind" ].as_i == 0 }
1191+ .group_by { |i | i[" pubkey" ].as_s }
1192+ .reject { |p , _ | p == pk }
1193+ .map { |_ , g | g.max_by { |i | i[" created_at" ].as_i } }
1194+ puts(" profile events of commenters = #{ comments_profile_events.size } " )
1195+
1196+ request_author_profile_events_command = (
1197+ [nak, " --since" , " 0" , " -a" , pk] + profile_kinds.map { |k | " -k #{ k } " } + input_relays
1198+ ).join(' ' )
1199+ trace(request_author_profile_events_command)
1200+ author_profile_events = parse_nostr_events(` #{ request_author_profile_events_command } ` )
1201+ .select { |i | profile_kinds.includes?(i[" kind" ].as_i) }
1202+ puts(" author profile events = #{ author_profile_events.size } " )
1203+ nak_raw([" event" ] + profile_relays, (comments_profile_events + author_profile_events).map { |i | i.to_json })
1204+ end
1205+ puts(" finished writing events" )
1206+ end
1207+
1208+ def nak_raw (args, input : Array (String ) = [] of String )
1209+ # TODO: exit after timeout
1210+ output = Channel (Tuple (String , Process ::Status )).new
1211+ trace(" running nak #{ args } " )
1212+ spawn do
1213+ value = Process .run(command: " nak" , args: args) do |p |
1214+ input.each do |line |
1215+ trace(" writing stdin" )
1216+ p.input.puts(line)
1217+ trace(" writing stdin ok" )
1218+ end
1219+ trace(" closing stdin" )
1220+ p.input.close
1221+ trace(" closed stdin" )
1222+ trace(" reading stdout" )
1223+ result = p.output.gets_to_end
1224+ trace(" finished reading stdout" )
1225+ result
1226+ end
1227+ trace(" sending stdout" )
1228+ output.send({value.strip, $? })
1229+ trace(" sent stdout" )
1230+ end
1231+ trace(" waiting for stdout" )
1232+ value, status = output.receive
1233+ trace(" received stdout" )
1234+ raise " #{ args } : unexpected exit code #{ status } , value=#{ value } " unless status.success?
1235+ value
1236+ end
1237+
1238+ def parse_nostr_events (text )
1239+ text
1240+ .split('\n' )
1241+ .reject { |i | i.strip.empty? }
1242+ .to_set
1243+ .map { |i | JSON .parse(i) }
1244+ end
1245+
11231246def processes
11241247 Dir .entries(" /proc" )
11251248 .select { |entry | entry =~ /^\d +$/ }
0 commit comments