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.
@@ -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
@@ -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)
@@ -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
- def refresh!
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?
@@ -1,3 +1,3 @@
1
1
  module Tempest
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -38,10 +38,11 @@ module Tempest
38
38
  end
39
39
 
40
40
  def perform
41
- response = yield(@session.access_jwt)
41
+ attempted_jwt = @session.access_jwt
42
+ response = yield(attempted_jwt)
42
43
 
43
- if response.unauthorized?
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.0
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