twterm 1.0.9 → 1.0.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b7f3f957fa6bb231072502fd2794d212db4547e
4
- data.tar.gz: 83aecc6b84ed61620f52b69f6b65c91b6d1af67b
3
+ metadata.gz: 8d067153f4c632aeba6108ea07201aff1db4790c
4
+ data.tar.gz: d6ddac0119221c40646bae8bd53bf19081ddab1b
5
5
  SHA512:
6
- metadata.gz: e3b1748a40af7f4a32b93568422e0cd65dbe6a9e7a12818567e8aff5142639af6396df93febdffd0f93a19afd9020bf4ac5f86ad468f60b00c5e8cdb597ee5ee
7
- data.tar.gz: 6df0822a5f0d59a9f1e2e260e2e43a2b5997a7a36426f5dc786f85283514c9d44ddd9ca74dc41fd7ff1fbc83085bf2eb84a6b4af8b12f33b3438a91ec7d3438c
6
+ metadata.gz: dcd02ff35df547ca1383dfe70ac5a097c4ca0d91448ec0a79a4557084a84a14b24961c190697d30f428b1053db710fcd4030fcb9350a0a12111dd1c6eeec0dba
7
+ data.tar.gz: d37de57e07ad39327df4864b961a141e146a00d6956f43391c2e9cf4843e5d4d80a78d4666204edfeb32c4fbc7dd0ab73692a6e847a3d060bb4a57271629848c
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --color
File without changes
data/README.md CHANGED
@@ -8,7 +8,7 @@ A full-featured CLI Twitter client
8
8
 
9
9
  ## Requirements
10
10
 
11
- - Ruby (compiled with C Curses and Readline)
11
+ - Ruby (>= 2.1, compiled with C Curses and Readline)
12
12
  - C curses
13
13
  - Readline
14
14
 
@@ -18,12 +18,34 @@ A full-featured CLI Twitter client
18
18
  $ gem install twterm
19
19
  ```
20
20
 
21
- ## Execution
21
+ ## Usage
22
+
23
+ To launch twterm, just type in your console:
22
24
 
23
25
  ```
24
26
  $ twterm
25
27
  ```
26
28
 
29
+ ### Basic key assignments
30
+
31
+ key | operation
32
+ --- | ---
33
+ `F` | add to favorite
34
+ `d` `C-d` | scroll down
35
+ `h` `C-b` `←` | show previous tab
36
+ `j` `C-n` `↓` | move down
37
+ `k` `C-p` `↑` | move up
38
+ `l` `C-f` `→` | show next tab
39
+ `n` | compose new tweet
40
+ `N` | open new tab
41
+ `r` | reply
42
+ `R` | retweet
43
+ `w` | close current tab
44
+ `Q` | quit
45
+ `?` | open key assignments cheatsheet
46
+
47
+ Type `?` key to see the full list of key assignments.
48
+
27
49
  ## License
28
50
 
29
51
  See the LICENSE file for license rights and limitations (MIT).
data/lib/twterm/app.rb CHANGED
@@ -7,13 +7,10 @@ module Twterm
7
7
  def initialize
8
8
  Dir.mkdir(DATA_DIR, 0700) unless File.directory?(DATA_DIR)
9
9
 
10
- Config.load
11
- Auth.authenticate_user if Config[:screen_name].nil?
10
+ Auth.authenticate_user(config) if config[:screen_name].nil?
12
11
 
13
12
  Screen.instance
14
13
 
15
- client = Client.new(Config[:user_id], Config[:screen_name], Config[:access_token], Config[:access_token_secret])
16
-
17
14
  timeline = Tab::TimelineTab.new(client)
18
15
  TabManager.instance.add_and_show(timeline)
19
16
 
@@ -53,6 +50,19 @@ module Twterm
53
50
 
54
51
  private
55
52
 
53
+ def client
54
+ @client ||= Client.new(
55
+ config[:user_id].to_i,
56
+ config[:screen_name],
57
+ config[:access_token],
58
+ config[:access_token_secret]
59
+ )
60
+ end
61
+
62
+ def config
63
+ @config ||= Config.new
64
+ end
65
+
56
66
  def run_periodic_cleanup
57
67
  Scheduler.new(300) do
58
68
  Status.cleanup
data/lib/twterm/auth.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Twterm
2
2
  module Auth
3
- def authenticate_user
3
+ def authenticate_user(config)
4
4
  consumer = OAuth::Consumer.new(
5
5
  'vLNSVFgXclBJQJRZ7VLMxL9lA',
6
6
  'OFLKzrepRG2p1hq0nUB9j2S9ndFQoNTPheTpmOY0GYw55jGgS5',
@@ -17,10 +17,10 @@ module Twterm
17
17
  pin = (STDIN.gets || '').strip
18
18
  access_token = request_token.get_access_token(oauth_verifier: pin)
19
19
 
20
- Config[:access_token] = access_token.token
21
- Config[:access_token_secret] = access_token.secret
22
- Config[:screen_name] = access_token.params[:screen_name]
23
- Config[:user_id] = access_token.params[:user_id]
20
+ config[:access_token] = access_token.token
21
+ config[:access_token_secret] = access_token.secret
22
+ config[:screen_name] = access_token.params[:screen_name]
23
+ config[:user_id] = access_token.params[:user_id]
24
24
  end
25
25
 
26
26
  module_function :authenticate_user
data/lib/twterm/client.rb CHANGED
@@ -3,146 +3,191 @@ module Twterm
3
3
  attr_reader :user_id, :screen_name
4
4
 
5
5
  CREATE_STATUS_PROC = -> (s) { Status.new(s) }
6
+ CONSUMER_KEY = 'vLNSVFgXclBJQJRZ7VLMxL9lA'.freeze
7
+ CONSUMER_SECRET = 'OFLKzrepRG2p1hq0nUB9j2S9ndFQoNTPheTpmOY0GYw55jGgS5'.freeze
6
8
 
7
9
  @@instances = []
8
10
 
9
- def initialize(user_id, screen_name, token, secret)
10
- @user_id = user_id
11
- @screen_name = screen_name
11
+ def connect_stream
12
+ stream_client.stop_stream
12
13
 
13
- @rest_client = Twitter::REST::Client.new do |config|
14
- config.consumer_key = 'vLNSVFgXclBJQJRZ7VLMxL9lA'
15
- config.consumer_secret = 'OFLKzrepRG2p1hq0nUB9j2S9ndFQoNTPheTpmOY0GYw55jGgS5'
16
- config.access_token = token
17
- config.access_token_secret = secret
14
+ @streaming_thread = Thread.new do
15
+ begin
16
+ Notifier.instance.show_message 'Trying to connect to Twitter...'
17
+ stream_client.userstream
18
+ rescue EventMachine::ConnectionError
19
+ Notifier.instance.show_error 'Connection failed'
20
+ sleep 30
21
+ retry
22
+ end
18
23
  end
24
+ end
19
25
 
20
- TweetStream.configure do |config|
21
- config.consumer_key = 'vLNSVFgXclBJQJRZ7VLMxL9lA'
22
- config.consumer_secret = 'OFLKzrepRG2p1hq0nUB9j2S9ndFQoNTPheTpmOY0GYw55jGgS5'
23
- config.oauth_token = token
24
- config.oauth_token_secret = secret
25
- config.auth_method = :oauth
26
+ def destroy_status(status)
27
+ send_request do
28
+ begin
29
+ rest_client.destroy_status(status.id)
30
+ yield if block_given?
31
+ rescue Twitter::Error::NotFound, Twitter::Error::Forbidden
32
+ Notifier.instance.show_error 'You cannot destroy that status'
33
+ end
26
34
  end
35
+ end
27
36
 
28
- @stream_client = TweetStream::Client.new
29
-
30
- @callbacks = {}
37
+ def favorite(status)
38
+ return false unless status.is_a? Status
31
39
 
32
- @mute_filter = -> _ { true }
33
- fetch_muted_users do |muted_user_ids|
34
- @mute_filter = lambda do |status|
35
- !muted_user_ids.include?(status.user.id) &&
36
- !(status.retweeted_status.is_a?(Twitter::NullObject) &&
37
- muted_user_ids.include?(status.retweeted_status.user.id))
38
- end
40
+ send_request do
41
+ rest_client.favorite(status.id)
42
+ status.favorite!
43
+ yield status if block_given?
39
44
  end
40
45
 
41
- @@instances << self
46
+ self
42
47
  end
43
48
 
44
- def stream
45
- @stream_client.on_friends do
46
- Notifier.instance.show_message 'Connection established' unless @stream_connected
47
- @stream_connected = true
49
+ def fetch_muted_users
50
+ send_request do
51
+ @muted_user_ids = rest_client.muted_ids.to_a
52
+ yield @muted_user_ids if block_given?
48
53
  end
54
+ end
49
55
 
50
- @stream_client.on_timeline_status do |tweet|
51
- status = Status.new(tweet)
52
- invoke_callbacks(:timeline_status, status)
53
- invoke_callbacks(:mention, status) if status.text.include? "@#{@screen_name}"
56
+ def home_timeline
57
+ send_request do
58
+ statuses = rest_client
59
+ .home_timeline(count: 100)
60
+ .select(&@mute_filter)
61
+ .map(&CREATE_STATUS_PROC)
62
+ yield statuses
54
63
  end
64
+ end
55
65
 
56
- @stream_client.on_delete do |status_id|
57
- timeline.delete_status(status_id)
58
- end
66
+ def initialize(user_id, screen_name, access_token, access_token_secret)
67
+ @user_id, @screen_name = user_id, screen_name
68
+ @access_token, @access_token_secret = access_token, access_token_secret
59
69
 
60
- @stream_client.on_event(:favorite) do |event|
61
- break if event[:source][:screen_name] == @screen_name
62
- message = "@#{event[:source][:screen_name]} has favorited your tweet: #{event[:target_object][:text]}"
63
- Notifier.instance.show_message(message)
64
- end
70
+ @callbacks = {}
65
71
 
66
- @stream_client.on_no_data_received do
67
- @stream_connected = false
68
- connect_stream
72
+ @mute_filter = -> _ { true }
73
+ fetch_muted_users do |muted_user_ids|
74
+ @mute_filter = lambda do |status|
75
+ !muted_user_ids.include?(status.user.id) &&
76
+ !(status.retweeted_status.is_a?(Twitter::NullObject) &&
77
+ muted_user_ids.include?(status.retweeted_status.user.id))
78
+ end
69
79
  end
70
80
 
71
- connect_stream
81
+ @@instances << self
72
82
  end
73
83
 
74
- def connect_stream
75
- @stream_client.stop_stream
76
-
77
- @streaming_thread = Thread.new do
78
- begin
79
- Notifier.instance.show_message 'Trying to connect to Twitter...'
80
- @stream_client.userstream
81
- rescue EventMachine::ConnectionError
82
- Notifier.instance.show_error 'Connection failed'
83
- sleep 30
84
- retry
85
- end
84
+ def list(list_id)
85
+ send_request do
86
+ yield List.new(rest_client.list(list_id))
86
87
  end
87
88
  end
88
89
 
89
- def post(text, in_reply_to = nil)
90
+ def list_timeline(list)
91
+ fail ArgumentError,
92
+ 'argument must be an instance of List class' unless list.is_a? List
90
93
  send_request do
91
- if in_reply_to.is_a? Status
92
- text = "@#{in_reply_to.user.screen_name} #{text}"
93
- @rest_client.update(text, in_reply_to_status_id: in_reply_to.id)
94
- else
95
- @rest_client.update(text)
96
- end
94
+ statuses = rest_client
95
+ .list_timeline(list.id, count: 100)
96
+ .select(&@mute_filter)
97
+ .map(&CREATE_STATUS_PROC)
98
+ yield statuses
97
99
  end
98
100
  end
99
101
 
100
- def home_timeline
102
+ def lists
101
103
  send_request do
102
- yield @rest_client.home_timeline(count: 100).select(&@mute_filter).map(&CREATE_STATUS_PROC)
104
+ yield rest_client.lists.map { |list| List.new(list) }
103
105
  end
104
106
  end
105
107
 
106
108
  def mentions
107
109
  send_request do
108
- yield @rest_client.mentions(count: 100).select(&@mute_filter).map(&CREATE_STATUS_PROC)
110
+ statuses = rest_client
111
+ .mentions(count: 100)
112
+ .select(&@mute_filter)
113
+ .map(&CREATE_STATUS_PROC)
114
+ yield statuses
109
115
  end
110
116
  end
111
117
 
112
- def user_timeline(user_id)
113
- send_request do
114
- yield @rest_client.user_timeline(user_id, count: 100).select(&@mute_filter).map(&CREATE_STATUS_PROC)
115
- end
118
+ def on_mention(&block)
119
+ fail ArgumentError, 'no block given' unless block_given?
120
+ on(:mention, &block)
116
121
  end
117
122
 
118
- def list(list_id)
123
+ def on_timeline_status(&block)
124
+ fail ArgumentError, 'no block given' unless block_given?
125
+ on(:timeline_status, &block)
126
+ end
127
+
128
+ def post(text, in_reply_to = nil)
119
129
  send_request do
120
- yield List.new(@rest_client.list(list_id))
130
+ if in_reply_to.is_a? Status
131
+ text = "@#{in_reply_to.user.screen_name} #{text}"
132
+ rest_client.update(text, in_reply_to_status_id: in_reply_to.id)
133
+ else
134
+ rest_client.update(text)
135
+ end
121
136
  end
122
137
  end
123
138
 
124
- def lists
125
- send_request do
126
- yield @rest_client.lists.map { |list| List.new(list) }
139
+ def rest_client
140
+ @rest_client ||= Twitter::REST::Client.new do |config|
141
+ config.consumer_key = CONSUMER_KEY
142
+ config.consumer_secret = CONSUMER_SECRET
143
+ config.access_token = @access_token
144
+ config.access_token_secret = @access_token_secret
127
145
  end
128
146
  end
129
147
 
130
- def list_timeline(list)
131
- fail ArgumentError, 'argument must be an instance of List class' unless list.is_a? List
148
+ def retweet(status)
149
+ fail ArgumentError,
150
+ 'argument must be an instance of Status class' unless status.is_a? Status
151
+
132
152
  send_request do
133
- yield @rest_client.list_timeline(list.id, count: 100).select(&@mute_filter).map(&CREATE_STATUS_PROC)
153
+ begin
154
+ rest_client.retweet!(status.id)
155
+ status.retweet!
156
+ yield status if block_given?
157
+ rescue => e
158
+ message =
159
+ case e
160
+ when Twitter::Error::AlreadyRetweeted
161
+ 'The status is already retweeted'
162
+ when Twitter::Error::NotFound
163
+ 'The status is not found'
164
+ when Twitter::Error::Forbidden
165
+ if status.user.id == user_id # when the status is mine
166
+ 'You cannot retweet your own status'
167
+ else # when the status is not mine
168
+ 'The status is protected'
169
+ end
170
+ else
171
+ raise e
172
+ end
173
+ Notifier.instance.show_error "Retweet attempt failed: #{message}"
174
+ end
134
175
  end
135
176
  end
136
177
 
137
178
  def search(query)
138
179
  send_request do
139
- yield @rest_client.search(query, count: 100).select(&@mute_filter).map(&CREATE_STATUS_PROC)
180
+ statuses = rest_client
181
+ .search(query, count: 100)
182
+ .select(&@mute_filter)
183
+ .map(&CREATE_STATUS_PROC)
184
+ yield statuses
140
185
  end
141
186
  end
142
187
 
143
188
  def show_status(status_id)
144
189
  send_request do
145
- yield Status.new(@rest_client.status(status_id))
190
+ yield Status.new(rest_client.status(status_id))
146
191
  end
147
192
  end
148
193
 
@@ -150,7 +195,7 @@ module Twterm
150
195
  send_request do
151
196
  user =
152
197
  begin
153
- User.new(@rest_client.user(query))
198
+ User.new(rest_client.user(query))
154
199
  rescue Twitter::Error::NotFound
155
200
  nil
156
201
  end
@@ -158,90 +203,82 @@ module Twterm
158
203
  end
159
204
  end
160
205
 
161
- def favorite(status)
162
- return false unless status.is_a? Status
206
+ def stream
207
+ stream_client.on_friends do
208
+ Notifier.instance.show_message 'Connection established' unless @stream_connected
209
+ @stream_connected = true
210
+ end
163
211
 
164
- send_request do
165
- @rest_client.favorite(status.id)
166
- status.favorite!
167
- yield status if block_given?
212
+ stream_client.on_timeline_status do |tweet|
213
+ status = Status.new(tweet)
214
+ invoke_callbacks(:timeline_status, status)
215
+ invoke_callbacks(:mention, status) if status.text.include? "@#{@screen_name}"
168
216
  end
169
217
 
170
- self
171
- end
218
+ stream_client.on_delete do |status_id|
219
+ timeline.delete_status(status_id)
220
+ end
172
221
 
173
- def unfavorite(status)
174
- fail ArgumentError, 'argument must be an instance of Status class' unless status.is_a? Status
222
+ stream_client.on_event(:favorite) do |event|
223
+ break if event[:source][:screen_name] == @screen_name
175
224
 
176
- send_request do
177
- @rest_client.unfavorite(status.id)
178
- status.unfavorite!
179
- yield status if block_given?
225
+ user = event[:source][:screen_name]
226
+ text = event[:target_object][:text]
227
+ message = "@#{user} has favorited your tweet: #{text}"
228
+ Notifier.instance.show_message(message)
180
229
  end
181
- end
182
230
 
183
- def retweet(status)
184
- return false unless status.is_a? Status
185
-
186
- send_request do
187
- begin
188
- @rest_client.retweet!(status.id)
189
- status.retweet!
190
- yield status if block_given?
191
- rescue Twitter::Error::AlreadyRetweeted, Twitter::Error::NotFound, Twitter::Error::Forbidden
192
- Notifier.instance.show_error 'Retweet attempt failed'
193
- end
231
+ stream_client.on_no_data_received do
232
+ @stream_connected = false
233
+ connect_stream
194
234
  end
235
+
236
+ connect_stream
195
237
  end
196
238
 
197
- def destroy_status(status)
198
- send_request do
199
- begin
200
- @rest_client.destroy_status(status.id)
201
- yield if block_given?
202
- rescue Twitter::Error::NotFound, Twitter::Error::Forbidden
203
- Notifier.instance.show_error 'You cannot destroy that status'
204
- end
205
- end
239
+ def stream_client
240
+ @stream_client ||= TweetStream::Client.new(
241
+ consumer_key: CONSUMER_KEY,
242
+ consumer_secret: CONSUMER_SECRET,
243
+ oauth_token: @access_token,
244
+ oauth_token_secret: @access_token_secret,
245
+ auth_method: :oauth
246
+ )
206
247
  end
207
248
 
208
- def fetch_muted_users
249
+ def unfavorite(status)
250
+ fail ArgumentError,
251
+ 'argument must be an instance of Status class' unless status.is_a? Status
252
+
209
253
  send_request do
210
- @muted_user_ids = @rest_client.muted_ids.to_a
211
- yield @muted_user_ids if block_given?
254
+ rest_client.unfavorite(status.id)
255
+ status.unfavorite!
256
+ yield status if block_given?
212
257
  end
213
258
  end
214
259
 
215
- def on_timeline_status(&block)
216
- fail ArgumentError, 'no block given' unless block_given?
217
- on(:timeline_status, &block)
260
+ def user_timeline(user_id)
261
+ send_request do
262
+ statuses = rest_client
263
+ .user_timeline(user_id, count: 100)
264
+ .select(&@mute_filter)
265
+ .map(&CREATE_STATUS_PROC)
266
+ yield statuses
267
+ end
218
268
  end
219
269
 
220
- def on_mention(&block)
221
- fail ArgumentError, 'no block given' unless block_given?
222
- on(:mention, &block)
270
+ def self.new(user_id, screen_name, token, secret)
271
+ detector = -> (instance) { instance.user_id == user_id }
272
+ instance = @@instances.find(&detector)
273
+ instance.nil? ? super : instance
223
274
  end
224
275
 
225
- class << self
226
- def new(user_id, screen_name, token, secret)
227
- detector = -> (instance) { instance.user_id == user_id }
228
- instance = @@instances.find(&detector)
229
- instance.nil? ? super : instance
230
- end
231
-
232
- def current
233
- @@instances[0]
234
- end
276
+ def self.current
277
+ @@instances[0]
235
278
  end
236
279
 
237
280
  private
238
281
 
239
- def on(event, &block)
240
- @callbacks[event] ||= []
241
- @callbacks[event] << block
242
- self
243
- end
244
-
245
282
  def invoke_callbacks(event, data = nil)
246
283
  return if @callbacks[event].nil?
247
284
 
@@ -249,6 +286,12 @@ module Twterm
249
286
  self
250
287
  end
251
288
 
289
+ def on(event, &block)
290
+ @callbacks[event] ||= []
291
+ @callbacks[event] << block
292
+ self
293
+ end
294
+
252
295
  def send_request(&block)
253
296
  Thread.new do
254
297
  begin
@@ -16,6 +16,17 @@ module Twterm
16
16
  transparent: -1
17
17
  }
18
18
 
19
+ def get_color_pair_index(fg, bg)
20
+ fail ArgumentError,
21
+ 'invalid color name for foreground' unless COLORS.include? fg
22
+ fail ArgumentError,
23
+ 'invalid color name for background' unless COLORS.include? bg
24
+
25
+ return @colors[bg][fg] unless @colors[bg][fg].nil?
26
+
27
+ add_color(fg, bg)
28
+ end
29
+
19
30
  def initialize
20
31
  @colors = {
21
32
  black: {}, white: {}, red: {}, green: {},
@@ -25,20 +36,13 @@ module Twterm
25
36
  @count = 0
26
37
  end
27
38
 
28
- def get_color_pair_index(fg, bg)
29
- fail ArgumentError, 'invalid color name' unless COLORS.include? fg
30
- fail ArgumentError, 'invalid color name' unless COLORS.include? bg
31
-
32
- return @colors[bg][fg] unless @colors[bg][fg].nil?
33
-
34
- add_color(fg, bg)
35
- end
36
-
37
39
  private
38
40
 
39
41
  def add_color(fg, bg)
40
- fail ArgumentError, 'invalid color name' unless COLORS.include? fg
41
- fail ArgumentError, 'invalid color name' unless COLORS.include? bg
42
+ fail ArgumentError,
43
+ 'invalid color name for foreground' unless COLORS.include? fg
44
+ fail ArgumentError,
45
+ 'invalid color name for background' unless COLORS.include? bg
42
46
 
43
47
  @count += 1
44
48
  index = @count
data/lib/twterm/config.rb CHANGED
@@ -1,32 +1,37 @@
1
1
  module Twterm
2
- module Config
3
- CONFIG_FILE = "#{App::DATA_DIR}/config"
4
-
2
+ class Config
5
3
  def [](key)
6
- @config[key]
4
+ config[key]
7
5
  end
8
6
 
9
7
  def []=(key, value)
10
- @config[key] = value
11
- save
8
+ return if config[key] == value
9
+ save_config_to_file
10
+ config[key] = value
11
+ end
12
+
13
+ private
14
+
15
+ def config
16
+ @config ||= exist_config_file? ? load_config_file : {}
12
17
  end
13
18
 
14
- def load
15
- unless File.exist? CONFIG_FILE
16
- @config = {}
17
- return
19
+ def save_config_to_file
20
+ File.open(config_file_path, 'w', 0600) do |f|
21
+ f.write config.to_yaml
18
22
  end
19
- @config = YAML.load(File.read(CONFIG_FILE)) || {}
20
23
  end
21
24
 
22
- private
25
+ def load_config_file
26
+ YAML.load_file(config_file_path)
27
+ end
23
28
 
24
- def save
25
- File.open(CONFIG_FILE, 'w', 0600) do |f|
26
- f.write @config.to_yaml
27
- end
29
+ def exist_config_file?
30
+ File.exist?(config_file_path)
28
31
  end
29
32
 
30
- module_function :[], :[]=, :load, :save
33
+ def config_file_path
34
+ "#{App::DATA_DIR}/config".freeze
35
+ end
31
36
  end
32
37
  end