-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path_publish-post.cr
executable file
·356 lines (296 loc) · 9.56 KB
/
_publish-post.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#!/usr/bin/env crystal
require "colorize"
require "http/client"
require "json"
require "uri"
require "yaml"
PROD_REPO_URI = "[email protected]:codonaft/codonaft.github.io.git"
PROD_BRANCH = "master"
ALLOWED_TIME_DIFF = 5.minutes
class PostMetadata
include YAML::Serializable
@[YAML::Field(key: "date")]
property raw_date : String
@[YAML::Field(key: "title")]
property title : String
@[YAML::Field(key: "permalink")]
property permalink : String
@[YAML::Field(key: "tags")]
property post_tags : Array(String)? # TODO: doesn't parse in all formats?
@[YAML::Field(key: "nostr")]
property nostr : YAML::Any?
def time
Time.parse!(@raw_date, "%Y-%m-%d %H:%M:%S %z")
end
def title
@title.gsub(/<[^>]+>/, "")
end
def tags
if @post_tags
@post_tags.not_nil!
else
[] of String
end
end
end
def main
if ARGV.size < 2
script = Path.new(__FILE__).relative_to(Dir.current)
puts("usage:")
puts(" BUNKER_URI='bunker://...' AUTHORIZED_KEY='...' #{script} _posts/1984-11-11-actions-are-louder-than-words.md \"So keep building\nthings.\"")
exit 1
end
repo_dir = File.dirname(File.realpath(__FILE__))
Dir.cd(repo_dir)
puts("current directory is #{repo_dir}")
post = ARGV[0]
message = ARGV[1]
bunker_uri = ENV["BUNKER_URI"]
authorized_key = ENV["AUTHORIZED_KEY"]
raise "Post file has changed" if `git status --porcelain #{post}`.starts_with?(" M ")
raise "git status failed" unless $?.success?
config = YAML.parse(File.read("_config.yml"))
post_base = File.basename(post).split('.')[0]
post_date = Time.parse(post_base[0..10], "%Y-%m-%d", Time::Location::UTC).date
raise "Unexpected posts dir" unless File.basename(File.dirname(post)) == "_posts"
posts_parent = File.dirname(File.dirname(File.realpath(post)))
lang = posts_parent == repo_dir ? "en" : File.basename(posts_parent)
supported_langs = config["defaults"]
.as_a
.map { |i| i["values"]["lang"]? }
.reject { |i| i.nil? }
.map { |i| i.not_nil!.as_s }
raise "Unexpected language #{lang}" unless supported_langs.includes?(lang)
nostr = config["theme_settings"]["nostr"]
pubkey = nak(["decode", nostr["npub"].as_s])["pubkey"]
community = nostr["community"]
_, raw_metadata, body = File.read(post).split("---", limit: 3)
metadata = PostMetadata.from_yaml(raw_metadata)
raise "nostr metadata is already set" if metadata.nostr
url = URI.parse(config["url"].as_s)
url.path = metadata.permalink.rchop('/')
content = "#{metadata.title}\n\n#{url.to_s}\n\n#{message}"
now = Time.utc
dt = now - metadata.time
raise "Unexpected delta time #{dt}" if dt.negative? || dt > ALLOWED_TIME_DIFF
raise "Inconsistent post filename/metadata date" unless post_date == metadata.time.date
lang_label = "ISO-639-1"
unsigned_event = {
"content" => content,
"created_at" => metadata.time.to_unix,
"kind" => 1,
"tags" => [
["a", "34550:#{pubkey}:#{community}"],
["r", url.to_s],
["subject", metadata.title],
["L", lang_label],
["l", lang, lang_label],
] + metadata.tags.map { |t| ["t", t.downcase] },
}.to_json
event = sign(unsigned_event, bunker_uri, authorized_key, nostr)
event_json = event.to_json
prod = event["pubkey"] == pubkey
Dir.mkdir_p(".backup")
backup_prefix = ".backup/#{url.path.gsub('/', '_')}"
event_backup = "#{backup_prefix}.#{now.to_unix}.nostr#{prod ? "" : ".test"}.json"
File.write(event_backup, event_json)
puts("saved event backup to #{event_backup}")
relays = prod ? nostr["relays"].as_a.map(&.to_s) : ["ws://localhost:8080"]
repo_uri = prod ? `git config --get remote.origin.url`.strip : Dir.current
raise "git config failed" unless $?.success?
branch = `git branch --show-current`.strip
raise "git branch failed" unless $?.success?
if prod
raise "Unexpected prod repo uri" unless repo_uri == PROD_REPO_URI
raise "Unexpected prod branch" unless branch == PROD_BRANCH
end
note = nak_raw(["encode", "note", event["id"].as_s])
new_raw_metadata = raw_metadata +
<<-STRING
nostr:
comments: #{note}
STRING
File.write(post, ["---", new_raw_metadata, "\n---", body].join)
Colorize.with.green.surround(STDOUT) do
puts(`git diff #{post}`.strip)
end
raise "git diff failed" unless $?.success?
puts
Colorize.with.yellow.surround(STDOUT) do
event.to_pretty_json(STDOUT)
end
puts
print("#{prod ? "PROD" : "TEST"} PUBLISH to #{repo_uri} (#{branch}) and #{relays}? [n] ")
if (gets || "").strip == "y"
puts("PUBLISHING...")
puts("commiting changes to git")
system("git add #{post} && git commit --no-gpg-sign --message 'Publish' #{post}")
loop do
begin
puts("pushing changes to git")
system("git push --force --repo #{repo_uri} origin #{branch}")
status = $?
raise "unexpected exit code #{status}" unless status.success?
await_successful_deployment(url)
puts("publishing nostr event #{event["id"]}")
puts(nak_raw(["event"] + relays, event_json))
puts("SUCCESS!")
break
rescue e
puts(e)
wait(1.seconds)
end
end
approve_and_broadcast(nostr, pubkey, event, backup_prefix, prod, relays, bunker_uri, authorized_key)
puts("checking video availability")
begin
media_url = config["theme_settings"]["p2p_player"]["media_urls"].as_a[0].as_s
video_url = `wget -qO - #{url.to_s} | grep --extended-regexp --only-matching '#{media_url}/.*m3u8'`
video_ok = HTTP::Client.get(video_url).status.success?
puts("#{video_url} is #{video_ok ? "ok" : "FAILED"}")
rescue e
puts(e)
end
else
system("git", ["checkout", post])
puts("exiting")
end
puts("FINISHED")
end
def nak_raw(args, input = nil)
# TODO: exit after timeout
output = Channel(Tuple(String, Process::Status)).new
spawn do
value = Process.run(command: "nak", args: args) do |p|
if input
p.input.puts(input)
p.input.close
end
p.output.gets_to_end
end
output.send({value.strip, $?})
end
value, status = output.receive
raise "#{args}: unexpected exit code #{status}, value=#{value}" unless status.success?
value
end
def nak(args, input = nil)
value = nak_raw(args, input)
begin
value = "{}" if value.empty?
JSON.parse(value)
rescue e
puts(value)
raise e
end
end
def sign(unsigned_event : String, bunker_uri : String, authorized_key : String, nostr)
event = nak([
"event",
"--pow", nostr["min_read_pow"].to_s,
"--connect", bunker_uri,
"--connect-as", authorized_key,
], unsigned_event)
# nak(["verify"], event.to_json) # FIXME: fails due to incorrect string escaping?
event
end
def approve_and_broadcast(nostr, pubkey, event, backup_prefix, prod, relays, bunker_uri, authorized_key)
puts("approving own post")
community = nostr["community"]
now = Time.utc
unsigned_approval_event = {
"created_at" => now.to_unix,
"kind" => 4550,
"tags" => [
["a", "34550:#{pubkey}:#{community}"],
["e", event["id"]],
["p", event["pubkey"]],
["k", event["kind"].to_s],
],
"content" => event.to_json,
}.to_json
approval_event = sign(unsigned_approval_event, bunker_uri, authorized_key, nostr)
approval_event_backup = "#{backup_prefix}.#{now.to_unix}.nostr.approval#{prod ? "" : ".test"}.json"
File.write(approval_event_backup, approval_event.to_json)
puts("saved event backup to #{approval_event_backup}")
puts
Colorize.with.yellow.surround(STDOUT) do
approval_event_backup.to_pretty_json(STDOUT)
end
puts
print("#{prod ? "PROD" : "TEST"} PUBLISH APPROVAL to #{relays}? [n] ")
if (gets || "").strip == "y"
loop do
begin
puts("publishing approval nostr event #{approval_event["id"]} for #{event["id"]}")
puts(nak_raw(["event"] + relays, approval_event.to_json))
puts("SUCCESS!")
break
rescue e
puts(e)
wait(1.seconds)
end
end
end
# FIXME: hangs forever
failed_relays = relays.reject do |relay|
begin
found_event(event, relay) && found_event(approval_event, relay)
rescue e
false
end
end
puts("failed to publish to relays: #{failed_relays}") unless failed_relays.empty?
if prod
print("broadcast both events? [n] ")
if (gets || "").strip == "y"
broadcast(event, failed_relays)
broadcast(approval_event, failed_relays)
end
puts("FINISHED")
end
end
def broadcast(event : JSON::Any, failed_relays : Array(String))
other_relays = begin
File.read_lines("_relays.txt")
.map { |i| i.strip }
.to_set
rescue e
Set(String).new
end
begin
relays_to_lookup = other_relays + failed_relays.to_set
puts("looking for #{event["id"]} in #{relays_to_lookup.size}")
relays = relays_to_lookup
.reject { |i|
begin
i.empty? || found_event(event, i)
rescue e
false
end
}
.to_a
puts("broadcasting #{event["id"]} to #{relays.size}")
puts(nak_raw(["event"] + relays, event.to_json))
rescue e
puts("failed to broadcast #{event.to_json}")
puts(e)
end
end
def found_event(event : JSON::Any, relay : String)
nak(["req", "-i", event["id"].to_json, relay])["id"] == event["id"]
end
def await_successful_deployment(url : URI)
wait(50.seconds)
loop do
status = HTTP::Client.get(url).status
puts("#{url} responded with #{status}")
break if status.success? || status.redirection?
wait(10.seconds)
end
end
def wait(time : Time::Span)
puts("waiting #{time}")
sleep(time)
end
main