@@ -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+ profile = ARGV .size > 1 && ARGV [1 ] == " profile"
79+ sync_nostr(config, profile: profile, output_relays: [" ws://localhost:7777" , " wss://nostr.codonaft.com" ])
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
@@ -461,7 +468,7 @@ def health(config)
461468 .map { |i | i.strip }
462469 .reject { |i | i.empty? }
463470 .map { |i | URI .parse(i) }
464- .reject { |i | i.hostname.nil? || i.hostname == " 127.0.0.1 " } + relay_mirrors
471+ .reject { |i | i.hostname.nil? || i.hostname == " localhost " } + relay_mirrors
465472 wss_uris = (trackers + other_relays + ` git grep --only-matching 'wss://[a-zA-Z0-9/._-]*'`
466473 .split('\n' )
467474 .map { |i | i.strip.gsub(/.*wss:\/\/ / , " " ) }
@@ -1120,6 +1127,98 @@ def check_manual_upload(host : String, *, owner : String, group : String, mode :
11201127 end
11211128end
11221129
1130+ def sync_nostr (config, * , profile : Bool , output_relays : Array (String ))
1131+ now = Time .utc
1132+ from = now - 3 .years # TODO: last commit date? last blog date? previous blog date?
1133+ to = now + 15 .minutes
1134+
1135+ nostr_config = config[" theme_settings" ][" nostr" ]
1136+ npub = nostr_config[" npub" ].as_s
1137+ pk = JSON .parse(` nak decode #{ npub } ` )[" pubkey" ].as_s
1138+
1139+ profile_relays = nostr_config[" profile_relays" ].as_a.map { |i | i.as_s }
1140+ relays = nostr_config[" relays" ].as_a.map { |i | i.as_s }
1141+ read_cache_relays = nostr_config[" read_cache_relays" ].as_a.map { |i | i.as_s }
1142+ input_relays : Array (String ) = relays + read_cache_relays + profile_relays
1143+
1144+ step(" sync-nostr" )
1145+ trace(" pk=#{ pk } input_relays=#{ input_relays } output_relays=#{ output_relays } " )
1146+
1147+ nak = " nak req --limit 1000 --since #{ from.to_unix } --until #{ to.to_unix } "
1148+
1149+ mentions = parse_nostr_events(` #{ nak } -p #{ pk } #{ input_relays.join(' ' ) } ` )
1150+ .reject { |i |
1151+ return false unless i[" tags" ].as_a.any? { |t | t.size > 1 && t[0 ] == " p" && t[1 ] == pk }
1152+ k = i[" kind" ].as_i
1153+ [3 , 1984 , 4454 ].includes?(k) || (k >= 5000 && k <= 7000 ) || (k >= 10000 && k <= 10102 ) || (k >= 20000 && k < 30000 ) || (k >= 30000 && k <= 30267 )
1154+ }
1155+ puts(" fetched #{ mentions.size } mentions" )
1156+
1157+ parsed_authored_events = parse_nostr_events(` #{ nak } -a #{ pk } #{ input_relays.join(' ' ) } ` )
1158+ authored_events = parsed_authored_events
1159+ .reject { |i |
1160+ return false unless i[" pubkey" ].as_s == pk
1161+ k = i[" kind" ].as_i
1162+ [4454 , 10044 ].includes?(k) || (k >= 20000 && k < 30000 )
1163+ }
1164+ puts(" fetched #{ authored_events.size } authored events" )
1165+
1166+ puts(" writing events" )
1167+ nak_raw([" event" ] + output_relays, (mentions + authored_events).map { |i | i.to_json })
1168+
1169+ if profile
1170+ profile_events = parsed_authored_events.select { |i |
1171+ k = i[" kind" ].as_i
1172+ [0 , 3 , 10002 ].includes?(k)
1173+ }
1174+ .group_by { |i | i[" kind" ].as_i }
1175+ .map { |k , g | g.max_by { |i | i[" created_at" ].as_i } }
1176+
1177+ puts(" writing #{ profile_events.size } profile events" )
1178+ nak_raw([" event" ] + profile_relays, profile_events.map { |i | i.to_json })
1179+ end
1180+
1181+ puts(" finished writing events" )
1182+ end
1183+
1184+ def nak_raw (args, input : Array (String ) = [] of String )
1185+ # TODO: exit after timeout
1186+ output = Channel (Tuple (String , Process ::Status )).new
1187+ trace(" running nak #{ args } " )
1188+ spawn do
1189+ value = Process .run(command: " nak" , args: args) do |p |
1190+ input.each do |line |
1191+ trace(" writing stdin" )
1192+ p.input.puts(line)
1193+ trace(" writing stdin ok" )
1194+ end
1195+ trace(" closing stdin" )
1196+ p.input.close
1197+ trace(" closed stdin" )
1198+ trace(" reading stdout" )
1199+ result = p.output.gets_to_end
1200+ trace(" finished reading stdout" )
1201+ result
1202+ end
1203+ trace(" sending stdout" )
1204+ output.send({value.strip, $? })
1205+ trace(" sent stdout" )
1206+ end
1207+ trace(" waiting for stdout" )
1208+ value, status = output.receive
1209+ trace(" received stdout" )
1210+ raise " #{ args } : unexpected exit code #{ status } , value=#{ value } " unless status.success?
1211+ value
1212+ end
1213+
1214+ def parse_nostr_events (text )
1215+ text
1216+ .split('\n' )
1217+ .reject { |i | i.strip.empty? }
1218+ .to_set
1219+ .map { |i | JSON .parse(i) }
1220+ end
1221+
11231222def processes
11241223 Dir .entries(" /proc" )
11251224 .select { |entry | entry =~ /^\d +$/ }
0 commit comments