tempest-rb 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +19 -0
- data/lib/tempest/avatar_store.rb +9 -0
- data/lib/tempest/cli.rb +72 -257
- data/lib/tempest/commands/base.rb +69 -0
- data/lib/tempest/commands/feed.rb +137 -0
- data/lib/tempest/commands/post.rb +101 -0
- data/lib/tempest/commands/tui.rb +337 -0
- data/lib/tempest/commands/whoami.rb +31 -0
- data/lib/tempest/commands.rb +6 -0
- data/lib/tempest/date_filter.rb +34 -0
- data/lib/tempest/handle_lookup.rb +14 -0
- data/lib/tempest/output/json_writer.rb +25 -0
- data/lib/tempest/output/line_writer.rb +20 -0
- data/lib/tempest/post.rb +3 -1
- data/lib/tempest/post_view.rb +69 -0
- data/lib/tempest/repl/dispatcher.rb +1 -1
- data/lib/tempest/repl/formatter.rb +1 -1
- data/lib/tempest/repl/runner.rb +24 -1
- data/lib/tempest/repl/screen.rb +59 -0
- data/lib/tempest/session.rb +35 -3
- data/lib/tempest/version.rb +1 -1
- data/lib/tempest/xrpc_client.rb +12 -3
- metadata +12 -1
data/lib/tempest/repl/runner.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Tempest
|
|
|
18
18
|
:timeline Fetch and print the home timeline
|
|
19
19
|
:stream on|off Toggle the Jetstream live feed
|
|
20
20
|
:open $LX Open the URL with id $LX in the browser
|
|
21
|
+
:relogin Re-authenticate when the cached session is dead
|
|
21
22
|
:help Show this help
|
|
22
23
|
:quit Exit tempest (or Ctrl-D)
|
|
23
24
|
|
|
@@ -28,10 +29,12 @@ module Tempest
|
|
|
28
29
|
|
|
29
30
|
DEFAULT_OPENER = ->(url) { system("open", url) }
|
|
30
31
|
|
|
32
|
+
RELOGIN_HINT = "type :relogin to re-authenticate".freeze
|
|
33
|
+
|
|
31
34
|
def initialize(session:, client:, input:, output:, dispatcher: Dispatcher.new,
|
|
32
35
|
stream_manager: nil, handle_resolver: nil, stream_output: nil,
|
|
33
36
|
timeline_store: nil, registry: Registry.new, opener: DEFAULT_OPENER,
|
|
34
|
-
avatar_store: nil)
|
|
37
|
+
avatar_store: nil, reauth: nil)
|
|
35
38
|
@session = session
|
|
36
39
|
@client = client
|
|
37
40
|
@input = input
|
|
@@ -44,6 +47,7 @@ module Tempest
|
|
|
44
47
|
@registry = registry
|
|
45
48
|
@opener = opener
|
|
46
49
|
@avatar_store = avatar_store
|
|
50
|
+
@reauth = reauth
|
|
47
51
|
# URIs already printed via bootstrap_timeline or backfill_timeline.
|
|
48
52
|
# Jetstream's cursor-replay can re-emit those same posts on startup
|
|
49
53
|
# (the persisted cursor is older than the getTimeline window), so the
|
|
@@ -104,6 +108,8 @@ module Tempest
|
|
|
104
108
|
handle_reply(command.args[0], command.args[1])
|
|
105
109
|
when :open
|
|
106
110
|
handle_open(command.args.first)
|
|
111
|
+
when :relogin
|
|
112
|
+
handle_relogin
|
|
107
113
|
when :unknown
|
|
108
114
|
@output.puts "unknown command: :#{command.args.first}"
|
|
109
115
|
end
|
|
@@ -133,10 +139,25 @@ module Tempest
|
|
|
133
139
|
def handle_post(text)
|
|
134
140
|
response = Post.create(@client, did: @session.did, text: text)
|
|
135
141
|
@output.puts "posted: #{response["uri"]}"
|
|
142
|
+
rescue Tempest::AuthenticationError => e
|
|
143
|
+
@output.puts "error: #{e.message} (#{RELOGIN_HINT})"
|
|
136
144
|
rescue Tempest::Error => e
|
|
137
145
|
@output.puts "error: #{e.message}"
|
|
138
146
|
end
|
|
139
147
|
|
|
148
|
+
def handle_relogin
|
|
149
|
+
if @reauth.nil?
|
|
150
|
+
@output.puts "relogin is not available in this session"
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
new_session = @reauth.call
|
|
155
|
+
@session.replace_with!(new_session)
|
|
156
|
+
@output.puts "signed in as @#{@session.handle}"
|
|
157
|
+
rescue Tempest::Error => e
|
|
158
|
+
@output.puts "relogin failed: #{e.message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
140
161
|
def handle_reply(var, body)
|
|
141
162
|
target = @registry.find_post(var)
|
|
142
163
|
if target.nil?
|
|
@@ -155,6 +176,8 @@ module Tempest
|
|
|
155
176
|
reply: { uri: reply_uri_for(target), cid: target.cid },
|
|
156
177
|
)
|
|
157
178
|
@output.puts "posted: #{response["uri"]}"
|
|
179
|
+
rescue Tempest::AuthenticationError => e
|
|
180
|
+
@output.puts "error: #{e.message} (#{RELOGIN_HINT})"
|
|
158
181
|
rescue Tempest::Error => e
|
|
159
182
|
@output.puts "error: #{e.message}"
|
|
160
183
|
end
|
data/lib/tempest/repl/screen.rb
CHANGED
|
@@ -20,6 +20,7 @@ module Tempest
|
|
|
20
20
|
@cols = cols
|
|
21
21
|
@enabled = false
|
|
22
22
|
@mutex = Mutex.new
|
|
23
|
+
@pending_resize = nil
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def enable
|
|
@@ -33,10 +34,13 @@ module Tempest
|
|
|
33
34
|
@io.print "\e[#{rows};1H" # park cursor on the final row (prompt)
|
|
34
35
|
@io.flush if @io.respond_to?(:flush)
|
|
35
36
|
@enabled = true
|
|
37
|
+
install_resize_trap
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def disable
|
|
39
41
|
return unless @enabled
|
|
42
|
+
uninstall_resize_trap
|
|
43
|
+
@io.print "\e_Ga=d,q=2\e\\"
|
|
40
44
|
@io.print "\e[r"
|
|
41
45
|
@io.flush if @io.respond_to?(:flush)
|
|
42
46
|
@enabled = false
|
|
@@ -46,8 +50,18 @@ module Tempest
|
|
|
46
50
|
@enabled
|
|
47
51
|
end
|
|
48
52
|
|
|
53
|
+
# SIGWINCH hook. Trap handlers in Ruby are restricted (can't reliably
|
|
54
|
+
# acquire mutexes or drive Reline), so we only stash the new dimensions
|
|
55
|
+
# here and apply them on the next mutex-protected write. If rows/cols
|
|
56
|
+
# are omitted (the production path), they're read from IO.console at
|
|
57
|
+
# apply time so coalesced WINCHes still pick up the latest size.
|
|
58
|
+
def notify_resize(rows: nil, cols: nil)
|
|
59
|
+
@pending_resize = { rows: rows, cols: cols }
|
|
60
|
+
end
|
|
61
|
+
|
|
49
62
|
def puts(*lines)
|
|
50
63
|
@mutex.synchronize do
|
|
64
|
+
apply_pending_resize
|
|
51
65
|
if @enabled
|
|
52
66
|
flat = lines.empty? ? [""] : lines.flat_map { |l| l.to_s.split("\n") }
|
|
53
67
|
flat.each { |line| insert_above_prompt(line) }
|
|
@@ -91,6 +105,50 @@ module Tempest
|
|
|
91
105
|
|
|
92
106
|
private
|
|
93
107
|
|
|
108
|
+
# Caller must hold @mutex. Re-issues DECSTBM and re-parks the cursor on
|
|
109
|
+
# the new prompt row when winsize actually changed; cheap no-op when it
|
|
110
|
+
# didn't (some terminals send spurious WINCHes on focus changes).
|
|
111
|
+
def apply_pending_resize
|
|
112
|
+
pending = @pending_resize
|
|
113
|
+
return unless pending
|
|
114
|
+
@pending_resize = nil
|
|
115
|
+
|
|
116
|
+
new_rows = pending[:rows] || detect_rows
|
|
117
|
+
new_cols = pending[:cols] || detect_cols
|
|
118
|
+
return unless new_rows && new_rows >= 4
|
|
119
|
+
return if new_rows == @rows && new_cols == @cols
|
|
120
|
+
|
|
121
|
+
@rows = new_rows
|
|
122
|
+
@cols = new_cols
|
|
123
|
+
return unless @enabled
|
|
124
|
+
|
|
125
|
+
@io.print "\e[1;#{@rows - 1}r"
|
|
126
|
+
@io.print "\e[#{@rows};1H"
|
|
127
|
+
@io.flush if @io.respond_to?(:flush)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Install a SIGWINCH trap that only flips a flag. Ruby's trap context
|
|
131
|
+
# forbids most blocking work (mutexes, IO that might re-enter Reline),
|
|
132
|
+
# so the actual DECSTBM reissue happens later when puts/print pick up
|
|
133
|
+
# the pending resize. The previous handler is saved so disable can
|
|
134
|
+
# restore it cleanly.
|
|
135
|
+
def install_resize_trap
|
|
136
|
+
return unless Signal.list.key?("WINCH")
|
|
137
|
+
screen = self
|
|
138
|
+
@previous_winch_trap = Signal.trap("WINCH") { screen.notify_resize }
|
|
139
|
+
rescue ArgumentError
|
|
140
|
+
# Some embedded Rubies refuse to trap WINCH; nothing to do.
|
|
141
|
+
@previous_winch_trap = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def uninstall_resize_trap
|
|
145
|
+
return unless Signal.list.key?("WINCH")
|
|
146
|
+
Signal.trap("WINCH", @previous_winch_trap || "DEFAULT")
|
|
147
|
+
@previous_winch_trap = nil
|
|
148
|
+
rescue ArgumentError
|
|
149
|
+
@previous_winch_trap = nil
|
|
150
|
+
end
|
|
151
|
+
|
|
94
152
|
def detect_rows
|
|
95
153
|
return nil unless defined?(IO) && IO.respond_to?(:console)
|
|
96
154
|
console = IO.console
|
|
@@ -130,6 +188,7 @@ module Tempest
|
|
|
130
188
|
|
|
131
189
|
def wrap_to_cols(line)
|
|
132
190
|
return [line] unless @cols && @cols.positive?
|
|
191
|
+
return [line] if line.include?("\e_G")
|
|
133
192
|
return [line] if Reline::Unicode.calculate_width(line, true) <= @cols
|
|
134
193
|
|
|
135
194
|
chunks, _ = Reline::Unicode.split_by_width(line, @cols)
|
data/lib/tempest/session.rb
CHANGED
|
@@ -46,6 +46,7 @@ module Tempest
|
|
|
46
46
|
@handle = handle
|
|
47
47
|
@pds_host = pds_host
|
|
48
48
|
@identifier = identifier
|
|
49
|
+
@refresh_mutex = Mutex.new
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def access_expired?
|
|
@@ -54,7 +55,40 @@ module Tempest
|
|
|
54
55
|
Time.now.to_i + EXPIRY_LEEWAY_SECONDS >= exp
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
# Adopts another Session's credentials in place. Used by :relogin so the
|
|
59
|
+
# XRPCClient that already holds a reference to this Session keeps working
|
|
60
|
+
# without having to be reconstructed.
|
|
61
|
+
def replace_with!(other)
|
|
62
|
+
@refresh_mutex.synchronize do
|
|
63
|
+
@access_jwt = other.access_jwt
|
|
64
|
+
@refresh_jwt = other.refresh_jwt
|
|
65
|
+
@did = other.did
|
|
66
|
+
@handle = other.handle
|
|
67
|
+
@pds_host = other.pds_host
|
|
68
|
+
end
|
|
69
|
+
@on_change&.call(self)
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Refreshes the session using the current refresh_jwt.
|
|
74
|
+
#
|
|
75
|
+
# When `if_unchanged_from:` is supplied, the refresh is skipped if the
|
|
76
|
+
# session's access_jwt has already moved past that value. Combined with the
|
|
77
|
+
# internal mutex, this lets concurrent callers coalesce a single
|
|
78
|
+
# refreshSession round-trip: the first caller refreshes while the rest wait
|
|
79
|
+
# for the lock and then observe the new token, no-op'ing instead of issuing
|
|
80
|
+
# duplicate refresh requests.
|
|
81
|
+
def refresh!(if_unchanged_from: nil)
|
|
82
|
+
@refresh_mutex.synchronize do
|
|
83
|
+
return self if if_unchanged_from && @access_jwt != if_unchanged_from
|
|
84
|
+
|
|
85
|
+
perform_refresh
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def perform_refresh
|
|
58
92
|
url = "#{@pds_host}/xrpc/com.atproto.server.refreshSession"
|
|
59
93
|
response = Tempest::HTTP.post_json(
|
|
60
94
|
url,
|
|
@@ -77,8 +111,6 @@ module Tempest
|
|
|
77
111
|
self
|
|
78
112
|
end
|
|
79
113
|
|
|
80
|
-
private
|
|
81
|
-
|
|
82
114
|
def jwt_exp(token)
|
|
83
115
|
_, payload, _ = token.split(".")
|
|
84
116
|
return nil if payload.nil?
|
data/lib/tempest/version.rb
CHANGED
data/lib/tempest/xrpc_client.rb
CHANGED
|
@@ -38,10 +38,11 @@ module Tempest
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def perform
|
|
41
|
-
|
|
41
|
+
attempted_jwt = @session.access_jwt
|
|
42
|
+
response = yield(attempted_jwt)
|
|
42
43
|
|
|
43
|
-
if response
|
|
44
|
-
@session.refresh!
|
|
44
|
+
if auth_expired_response?(response)
|
|
45
|
+
@session.refresh!(if_unchanged_from: attempted_jwt)
|
|
45
46
|
response = yield(@session.access_jwt)
|
|
46
47
|
end
|
|
47
48
|
|
|
@@ -49,5 +50,13 @@ module Tempest
|
|
|
49
50
|
|
|
50
51
|
response.body
|
|
51
52
|
end
|
|
53
|
+
|
|
54
|
+
def auth_expired_response?(response)
|
|
55
|
+
return true if response.unauthorized?
|
|
56
|
+
return false unless response.status == 400
|
|
57
|
+
return false unless response.body.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
response.body["error"] == "ExpiredToken"
|
|
60
|
+
end
|
|
52
61
|
end
|
|
53
62
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tempest-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yuya Fujiwara
|
|
@@ -94,11 +94,19 @@ files:
|
|
|
94
94
|
- lib/tempest.rb
|
|
95
95
|
- lib/tempest/avatar_store.rb
|
|
96
96
|
- lib/tempest/cli.rb
|
|
97
|
+
- lib/tempest/commands.rb
|
|
98
|
+
- lib/tempest/commands/base.rb
|
|
99
|
+
- lib/tempest/commands/feed.rb
|
|
100
|
+
- lib/tempest/commands/post.rb
|
|
101
|
+
- lib/tempest/commands/tui.rb
|
|
102
|
+
- lib/tempest/commands/whoami.rb
|
|
97
103
|
- lib/tempest/config.rb
|
|
98
104
|
- lib/tempest/cursor_store.rb
|
|
105
|
+
- lib/tempest/date_filter.rb
|
|
99
106
|
- lib/tempest/debug_log.rb
|
|
100
107
|
- lib/tempest/facet.rb
|
|
101
108
|
- lib/tempest/follows.rb
|
|
109
|
+
- lib/tempest/handle_lookup.rb
|
|
102
110
|
- lib/tempest/handle_resolver.rb
|
|
103
111
|
- lib/tempest/http.rb
|
|
104
112
|
- lib/tempest/id_var.rb
|
|
@@ -108,7 +116,10 @@ files:
|
|
|
108
116
|
- lib/tempest/jetstream/subscription.rb
|
|
109
117
|
- lib/tempest/jetstream/watchdog.rb
|
|
110
118
|
- lib/tempest/kitty.rb
|
|
119
|
+
- lib/tempest/output/json_writer.rb
|
|
120
|
+
- lib/tempest/output/line_writer.rb
|
|
111
121
|
- lib/tempest/post.rb
|
|
122
|
+
- lib/tempest/post_view.rb
|
|
112
123
|
- lib/tempest/repl/async_output.rb
|
|
113
124
|
- lib/tempest/repl/dispatcher.rb
|
|
114
125
|
- lib/tempest/repl/formatter.rb
|