@@ -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" , " 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
@@ -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,102 @@ 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+ # TODO: do the same thing as cron bash job that uses rust nostr cli client? connect to relays over tor?
1132+
1133+ now = Time .utc
1134+ from = now - 3 .years # TODO: last commit date? last blog date? previous blog date?
1135+ to = now + 15 .minutes
1136+
1137+ nostr_config = config[" theme_settings" ][" nostr" ]
1138+ npub = nostr_config[" npub" ].as_s
1139+ pk = JSON .parse(` nak decode #{ npub } ` )[" pubkey" ].as_s
1140+
1141+ profile_relays = nostr_config[" profile_relays" ].as_a.map { |i | i.as_s }
1142+ relays = nostr_config[" relays" ].as_a.map { |i | i.as_s }
1143+ read_cache_relays = nostr_config[" read_cache_relays" ].as_a.map { |i | i.as_s }
1144+ input_relays : Array (String ) = relays + read_cache_relays + profile_relays
1145+
1146+ step(" sync-nostr" )
1147+ trace(" pk=#{ pk } input_relays=#{ input_relays } output_relays=#{ output_relays } " )
1148+
1149+ nak = " nak req --limit 1000 --since #{ from.to_unix } --until #{ to.to_unix } "
1150+
1151+ mentions = parse_nostr_events(` #{ nak } -p #{ pk } #{ input_relays.join(' ' ) } ` )
1152+ .reject { |i |
1153+ return false unless i[" tags" ].as_a.any? { |t | t.size > 1 && t[0 ] == " p" && t[1 ] == pk }
1154+ k = i[" kind" ].as_i
1155+ [3 , 1984 , 4454 ].includes?(k) || (k >= 5000 && k <= 7000 ) || (k >= 10000 && k <= 10102 ) || (k >= 20000 && k < 30000 ) || (k >= 30000 && k <= 30267 )
1156+ }
1157+ puts(" fetched #{ mentions.size } mentions" )
1158+
1159+ parsed_authored_events = parse_nostr_events(` #{ nak } -a #{ pk } #{ input_relays.join(' ' ) } ` )
1160+ authored_events = parsed_authored_events
1161+ .reject { |i |
1162+ return false unless i[" pubkey" ].as_s == pk
1163+ k = i[" kind" ].as_i
1164+ [4454 , 10044 ].includes?(k) || (k >= 20000 && k < 30000 )
1165+ }
1166+ puts(" fetched #{ authored_events.size } authored events" )
1167+
1168+ # TODO: check spam
1169+
1170+ puts(" writing events" )
1171+ nak_raw([" event" ] + output_relays, (mentions + authored_events).map { |i | i.to_json })
1172+
1173+ if profile
1174+ profile_events = parsed_authored_events.select { |i |
1175+ k = i[" kind" ].as_i
1176+ [0 , 3 , 10002 ].includes?(k)
1177+ }
1178+ .group_by { |i | i[" kind" ].as_i }
1179+ .map { |k , g | g.max_by { |i | i[" created_at" ].as_i } }
1180+
1181+ puts(" writing #{ profile_events.size } profile events" )
1182+ nak_raw([" event" ] + profile_relays, profile_events.map { |i | i.to_json })
1183+ end
1184+
1185+ puts(" finished writing events" )
1186+ end
1187+
1188+ def nak_raw (args, input : Array (String ) = [] of String )
1189+ # TODO: exit after timeout
1190+ output = Channel (Tuple (String , Process ::Status )).new
1191+ trace(" running nak #{ args } " )
1192+ spawn do
1193+ value = Process .run(command: " nak" , args: args) do |p |
1194+ input.each do |line |
1195+ trace(" writing stdin" )
1196+ p.input.puts(line)
1197+ trace(" writing stdin ok" )
1198+ end
1199+ trace(" closing stdin" )
1200+ p.input.close
1201+ trace(" closed stdin" )
1202+ trace(" reading stdout" )
1203+ result = p.output.gets_to_end
1204+ trace(" finished reading stdout" )
1205+ result
1206+ end
1207+ trace(" sending stdout" )
1208+ output.send({value.strip, $? })
1209+ trace(" sent stdout" )
1210+ end
1211+ trace(" waiting for stdout" )
1212+ value, status = output.receive
1213+ trace(" received stdout" )
1214+ raise " #{ args } : unexpected exit code #{ status } , value=#{ value } " unless status.success?
1215+ value
1216+ end
1217+
1218+ def parse_nostr_events (text )
1219+ text
1220+ .split('\n' )
1221+ .reject { |i | i.strip.empty? }
1222+ .to_set
1223+ .map { |i | JSON .parse(i) }
1224+ end
1225+
11231226def processes
11241227 Dir .entries(" /proc" )
11251228 .select { |entry | entry =~ /^\d +$/ }
0 commit comments