twterm 1.0.9 → 1.0.10

Sign up to get free protection for your applications and to get access to all the features.
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