twterm 2.0.1 → 2.1.0
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/lib/twterm.rb +1 -1
- data/lib/twterm/app.rb +2 -2
- data/lib/twterm/completer/abstract_completer.rb +25 -0
- data/lib/twterm/completer/default_completer.rb +21 -0
- data/lib/twterm/completer/screen_name_completer.rb +17 -0
- data/lib/twterm/completer/search_query_completer.rb +180 -0
- data/lib/twterm/completion_manager.rb +37 -0
- data/lib/twterm/key_mapper.rb +14 -2
- data/lib/twterm/key_mapper/abstract_key_mapper.rb +10 -3
- data/lib/twterm/key_mapper/status_key_mapper.rb +1 -0
- data/lib/twterm/repository/abstract_entity_repository.rb +4 -0
- data/lib/twterm/repository/status_repository.rb +7 -5
- data/lib/twterm/repository/user_repository.rb +0 -4
- data/lib/twterm/rest_client.rb +7 -9
- data/lib/twterm/scheduler.rb +1 -0
- data/lib/twterm/status.rb +16 -1
- data/lib/twterm/tab/new/search.rb +2 -2
- data/lib/twterm/tab/new/user.rb +1 -1
- data/lib/twterm/tab/statuses/base.rb +9 -1
- data/lib/twterm/tab/statuses/cacheable.rb +18 -0
- data/lib/twterm/tab/statuses/conversation.rb +66 -1
- data/lib/twterm/tab/statuses/list_timeline.rb +2 -2
- data/lib/twterm/tab/statuses/user_timeline.rb +11 -2
- data/lib/twterm/tweetbox.rb +46 -57
- data/lib/twterm/version.rb +1 -1
- data/spec/fixtures/list.json +69 -0
- data/spec/twterm/completer/search_query_completer_spec.rb +231 -0
- data/twterm.gemspec +1 -1
- metadata +15 -8
- data/lib/twterm/completion_mamanger.rb +0 -42
data/lib/twterm/rest_client.rb
CHANGED
@@ -245,17 +245,15 @@ module Twterm
|
|
245
245
|
end
|
246
246
|
end
|
247
247
|
|
248
|
-
def post(text,
|
248
|
+
def post(text, options = {})
|
249
249
|
send_request do
|
250
|
-
|
251
|
-
user = user_repository.find(in_reply_to.user_id)
|
252
|
-
text = "@#{user.screen_name} #{text}"
|
253
|
-
rest_client.update(text, in_reply_to_status_id: in_reply_to.id)
|
254
|
-
else
|
255
|
-
rest_client.update(text)
|
256
|
-
end
|
257
|
-
publish(Event::Notification::Success.new('Your tweet has been posted'))
|
250
|
+
rest_client.update(text, options)
|
258
251
|
end
|
252
|
+
.then do |status|
|
253
|
+
publish(Event::Notification::Success.new('Your tweet has been posted'))
|
254
|
+
|
255
|
+
status
|
256
|
+
end
|
259
257
|
end
|
260
258
|
|
261
259
|
def rate_limit_status
|
data/lib/twterm/scheduler.rb
CHANGED
data/lib/twterm/status.rb
CHANGED
@@ -1,10 +1,19 @@
|
|
1
1
|
require 'concurrent'
|
2
2
|
|
3
|
+
class Twitter::Tweet
|
4
|
+
attr_reader :quoted_status_id
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
@quoted_status_id = args[0][:quoted_status_id]
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
3
12
|
module Twterm
|
4
13
|
class Status
|
5
14
|
attr_reader :created_at, :favorite_count, :favorited, :id,
|
6
15
|
:in_reply_to_status_id, :media, :retweet_count, :retweeted,
|
7
|
-
:retweeted_status_id, :text, :urls, :user_id
|
16
|
+
:quoted_status_id, :retweeted_status_id, :text, :url, :urls, :user_id
|
8
17
|
alias_method :favorited?, :favorited
|
9
18
|
alias_method :retweeted?, :retweeted
|
10
19
|
|
@@ -35,6 +44,8 @@ module Twterm
|
|
35
44
|
@text = CGI.unescapeHTML(tweet.full_text.dup)
|
36
45
|
@created_at = tweet.created_at.dup.localtime
|
37
46
|
@in_reply_to_status_id = tweet.in_reply_to_status_id
|
47
|
+
@quoted_status_id = tweet.quoted_status_id
|
48
|
+
@url = tweet.url
|
38
49
|
|
39
50
|
update!(tweet, is_retweeted_status)
|
40
51
|
|
@@ -48,6 +59,10 @@ module Twterm
|
|
48
59
|
expand_url!
|
49
60
|
end
|
50
61
|
|
62
|
+
def quote?
|
63
|
+
!quoted_status_id.nil?
|
64
|
+
end
|
65
|
+
|
51
66
|
def retweet?
|
52
67
|
!retweeted_status_id.nil?
|
53
68
|
end
|
@@ -36,9 +36,9 @@ module Twterm
|
|
36
36
|
|
37
37
|
input_thread = Thread.new do
|
38
38
|
close_screen
|
39
|
-
app.completion_manager.
|
39
|
+
app.completion_manager.set_search_mode!
|
40
40
|
puts "\ninput search query"
|
41
|
-
query = (readline('> ') || '').strip
|
41
|
+
query = (readline('> ', true) || '').strip
|
42
42
|
resetter.call
|
43
43
|
|
44
44
|
tab = query.nil? || query.empty? ? Tab::New::Search.new(app, client) : Tab::Statuses::Search.new(app, client, query)
|
data/lib/twterm/tab/new/user.rb
CHANGED
@@ -116,10 +116,16 @@ module Twterm
|
|
116
116
|
render
|
117
117
|
end
|
118
118
|
|
119
|
+
def quote
|
120
|
+
return if highlighted_status.nil?
|
121
|
+
|
122
|
+
app.tweetbox.quote(highlighted_original_status)
|
123
|
+
end
|
124
|
+
|
119
125
|
def reply
|
120
126
|
return if highlighted_status.nil?
|
121
127
|
|
122
|
-
app.tweetbox.
|
128
|
+
app.tweetbox.reply(highlighted_original_status)
|
123
129
|
end
|
124
130
|
|
125
131
|
def respond_to_key(key)
|
@@ -142,6 +148,8 @@ module Twterm
|
|
142
148
|
retweet
|
143
149
|
when k[:tab, :reload]
|
144
150
|
reload
|
151
|
+
when k[:status, :quote]
|
152
|
+
quote
|
145
153
|
when k[:status, :user]
|
146
154
|
show_user
|
147
155
|
else
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Twterm
|
2
|
+
module Tab
|
3
|
+
module Statuses
|
4
|
+
module Cacheable
|
5
|
+
def retrieve_from_cache!
|
6
|
+
statuses = cached_statuses
|
7
|
+
cached_statuses.each { |status| append(status) }
|
8
|
+
|
9
|
+
unless statuses.empty?
|
10
|
+
sort
|
11
|
+
scroller.move_to_top
|
12
|
+
initially_loaded!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,11 +1,14 @@
|
|
1
1
|
require 'concurrent'
|
2
2
|
|
3
|
+
require 'twterm/tab/dumpable'
|
3
4
|
require 'twterm/tab/statuses/base'
|
5
|
+
require 'twterm/tab/statuses/cacheable'
|
4
6
|
|
5
7
|
module Twterm
|
6
8
|
module Tab
|
7
9
|
module Statuses
|
8
10
|
class Conversation < Base
|
11
|
+
include Cacheable
|
9
12
|
include Dumpable
|
10
13
|
|
11
14
|
attr_reader :status_id
|
@@ -18,7 +21,10 @@ module Twterm
|
|
18
21
|
find_or_fetch_status(status_id).then do |status|
|
19
22
|
append(status)
|
20
23
|
fetch_ancestor(status)
|
24
|
+
fetch_quoted_status(status)
|
21
25
|
find_descendants(status)
|
26
|
+
fetch_possible_quotes(status)
|
27
|
+
fetch_possible_replies(status)
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
@@ -40,11 +46,62 @@ module Twterm
|
|
40
46
|
end
|
41
47
|
end
|
42
48
|
|
49
|
+
def fetch_possible_quotes(status)
|
50
|
+
client.search(status.url).then do |statuses|
|
51
|
+
statuses
|
52
|
+
.select { |s| !s.retweet? && s.quoted_status_id == status.id }
|
53
|
+
.each { |s| prepend(s) }
|
54
|
+
|
55
|
+
sort
|
56
|
+
render
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch_possible_replies(status)
|
61
|
+
user = app.user_repository.find(status.user_id)
|
62
|
+
|
63
|
+
return if user.nil?
|
64
|
+
|
65
|
+
client.search("to:#{user.screen_name}").then do |statuses|
|
66
|
+
statuses
|
67
|
+
.select { |s| !s.retweet? && s.in_reply_to_status_id == status.id }
|
68
|
+
.each { |s| prepend(s) }
|
69
|
+
|
70
|
+
sort
|
71
|
+
render
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def fetch_quoted_status(status)
|
76
|
+
quoted_status_id = status.quoted_status_id
|
77
|
+
|
78
|
+
if quoted_status_id.nil?
|
79
|
+
Concurrent::Promise.fulfill(nil)
|
80
|
+
elsif (instance = app.status_repository.find(quoted_status_id))
|
81
|
+
Concurrent::Promise.fulfill(instance)
|
82
|
+
else
|
83
|
+
client.show_status(quoted_status_id)
|
84
|
+
end
|
85
|
+
.then do |quoted_status|
|
86
|
+
next if quoted_status.nil?
|
87
|
+
append(quoted_status)
|
88
|
+
sort
|
89
|
+
fetch_ancestor(quoted_status)
|
90
|
+
fetch_quoted_status(quoted_status)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
43
94
|
def find_descendants(status)
|
44
|
-
app.status_repository.find_replies_for(status.id).each do |reply|
|
95
|
+
app.status_repository.find_replies_for(status.id).reject { |s| s.retweet? }.each do |reply|
|
45
96
|
prepend(reply)
|
46
97
|
find_descendants(reply)
|
47
98
|
end
|
99
|
+
|
100
|
+
app.status_repository.find_quotes_for(status.id).reject { |s| s.retweet? }.each do |quote|
|
101
|
+
prepend(quote)
|
102
|
+
find_descendants(quote)
|
103
|
+
end
|
104
|
+
|
48
105
|
sort
|
49
106
|
end
|
50
107
|
|
@@ -57,6 +114,8 @@ module Twterm
|
|
57
114
|
|
58
115
|
@status_id = status_id
|
59
116
|
|
117
|
+
retrieve_from_cache!
|
118
|
+
|
60
119
|
reload.then do
|
61
120
|
scroller.move_to_top
|
62
121
|
sort
|
@@ -66,6 +125,12 @@ module Twterm
|
|
66
125
|
def title
|
67
126
|
'Conversation'.freeze
|
68
127
|
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def cached_statuses
|
132
|
+
[app.status_repository.find(status_id)].compact
|
133
|
+
end
|
69
134
|
end
|
70
135
|
end
|
71
136
|
end
|
@@ -15,6 +15,8 @@ module Twterm
|
|
15
15
|
|
16
16
|
self.title = 'Loading...'.freeze
|
17
17
|
|
18
|
+
@auto_reloader = Scheduler.new(300) { reload }
|
19
|
+
|
18
20
|
find_or_fetch_list(list_id).then do |list|
|
19
21
|
self.title = list.full_name
|
20
22
|
app.tab_manager.refresh_window
|
@@ -23,8 +25,6 @@ module Twterm
|
|
23
25
|
initially_loaded!
|
24
26
|
scroller.move_to_top
|
25
27
|
end
|
26
|
-
|
27
|
-
@auto_reloader = Scheduler.new(300) { reload }
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'twterm/tab/statuses/base'
|
2
|
+
require 'twterm/tab/statuses/cacheable'
|
2
3
|
|
3
4
|
module Twterm
|
4
5
|
module Tab
|
5
6
|
module Statuses
|
6
7
|
class UserTimeline < Base
|
8
|
+
include Cacheable
|
7
9
|
include Dumpable
|
8
10
|
|
9
11
|
attr_reader :user, :user_id
|
@@ -29,6 +31,9 @@ module Twterm
|
|
29
31
|
super(app, client)
|
30
32
|
|
31
33
|
@user_id = user_id
|
34
|
+
@auto_reloader = Scheduler.new(120) { reload }
|
35
|
+
|
36
|
+
retrieve_from_cache!
|
32
37
|
|
33
38
|
find_or_fetch_user(user_id).then do |user|
|
34
39
|
@user = user
|
@@ -38,14 +43,18 @@ module Twterm
|
|
38
43
|
initially_loaded!
|
39
44
|
scroller.move_to_top
|
40
45
|
end
|
41
|
-
|
42
|
-
@auto_reloader = Scheduler.new(120) { reload }
|
43
46
|
end
|
44
47
|
end
|
45
48
|
|
46
49
|
def title
|
47
50
|
@user.nil? ? 'Loading...' : "@#{@user.screen_name} timeline"
|
48
51
|
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def cached_statuses
|
56
|
+
app.status_repository.all.select { |status| status.user_id == user_id }
|
57
|
+
end
|
49
58
|
end
|
50
59
|
end
|
51
60
|
end
|
data/lib/twterm/tweetbox.rb
CHANGED
@@ -15,109 +15,98 @@ module Twterm
|
|
15
15
|
@app, @client = app, client
|
16
16
|
end
|
17
17
|
|
18
|
-
def compose
|
19
|
-
|
20
|
-
|
21
|
-
@in_reply_to, screen_name =
|
22
|
-
if in_reply_to.is_a?(Status)
|
23
|
-
[in_reply_to, app.user_repository.find(in_reply_to.user_id).screen_name]
|
24
|
-
else
|
25
|
-
[nil, nil]
|
26
|
-
end
|
18
|
+
def compose
|
19
|
+
ask_and_post("\e[1mCompose new Tweet\e[0m", '> ', -> body { body })
|
20
|
+
end
|
27
21
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
22
|
+
def quote(status)
|
23
|
+
screen_name = app.user_repository.find(status.user_id).screen_name
|
24
|
+
leading_text = "\e[1mQuoting @#{screen_name}'s Tweet\e[0m\n\n#{status.text}"
|
25
|
+
prompt = '> '
|
33
26
|
|
34
|
-
|
35
|
-
|
27
|
+
ask_and_post(leading_text, prompt, -> body { "#{body} #{status.url}" })
|
28
|
+
end
|
36
29
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
30
|
+
def reply(status)
|
31
|
+
screen_name = app.user_repository.find(status.user_id).screen_name
|
32
|
+
leading_text = "\e[1mReplying to @#{screen_name}\e[0m\n\n#{status.text}"
|
33
|
+
prompt = "> @#{screen_name} "
|
42
34
|
|
43
|
-
|
35
|
+
ask_and_post(leading_text, prompt, -> body { "@#{screen_name} #{body}" }, { in_reply_to_status_id: status.id })
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :app, :client, :in_reply_to
|
41
|
+
|
42
|
+
def ask(prompt, postprocessor, &cont)
|
43
|
+
app.completion_manager.set_default_mode!
|
44
|
+
|
45
|
+
thread = Thread.new do
|
46
|
+
raw_text = ''
|
44
47
|
|
45
48
|
loop do
|
46
49
|
loop do
|
47
|
-
|
48
|
-
line = (readline(msg, true) || '').strip
|
50
|
+
line = (readline(prompt, true) || '').strip
|
49
51
|
break if line.empty?
|
50
52
|
|
51
53
|
if line.end_with?('\\')
|
52
|
-
|
54
|
+
raw_text << line.chop.rstrip + "\n"
|
53
55
|
else
|
54
|
-
|
56
|
+
raw_text << line
|
55
57
|
break
|
56
58
|
end
|
57
59
|
end
|
58
60
|
|
59
61
|
puts "\n"
|
60
62
|
|
63
|
+
text = postprocessor.call(raw_text)
|
64
|
+
|
61
65
|
begin
|
62
|
-
|
66
|
+
validate!(text)
|
63
67
|
break
|
64
68
|
rescue EmptyTextError
|
65
69
|
break
|
66
70
|
rescue InvalidCharactersError
|
67
71
|
puts 'Text contains invalid characters'
|
68
72
|
rescue TextTooLongError
|
69
|
-
puts "Text is too long (#{text_length} / 140 characters)"
|
73
|
+
puts "Text is too long (#{text_length(text)} / 140 characters)"
|
70
74
|
end
|
71
75
|
|
72
76
|
puts "\n"
|
73
|
-
|
77
|
+
raw_text = ''
|
74
78
|
end
|
75
79
|
|
76
|
-
|
77
|
-
|
80
|
+
reset
|
81
|
+
cont.call(raw_text) unless raw_text.empty?
|
78
82
|
end
|
79
83
|
|
80
84
|
app.register_interruption_handler do
|
81
85
|
thread.kill
|
82
|
-
clear
|
83
86
|
puts "\nCanceled"
|
84
|
-
|
87
|
+
reset
|
85
88
|
end
|
86
89
|
|
87
90
|
thread.join
|
88
91
|
end
|
89
92
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
def clear
|
95
|
-
@text = ''
|
96
|
-
@in_reply_to = nil
|
97
|
-
end
|
98
|
-
|
99
|
-
def post
|
100
|
-
validate_text!
|
101
|
-
client.post(text, in_reply_to)
|
102
|
-
rescue EmptyTextError
|
103
|
-
# do nothing
|
104
|
-
rescue InvalidCharactersError
|
105
|
-
publish(Event::Notification::Error.new('Text contains invalid characters'))
|
106
|
-
rescue TextTooLongError
|
107
|
-
publish(Event::Notification::Error.new("Text is too long (#{text_length} / 140 characters)"))
|
108
|
-
ensure
|
109
|
-
clear
|
93
|
+
def ask_and_post(leading_text, prompt, postprocessor, options = {})
|
94
|
+
close_screen
|
95
|
+
puts "\e[H\e[2J#{leading_text}\n\n"
|
96
|
+
ask(prompt, postprocessor) { |text| client.post(postprocessor.call(text), options) }
|
110
97
|
end
|
111
98
|
|
112
|
-
def
|
113
|
-
|
99
|
+
def reset
|
100
|
+
reset_prog_mode
|
101
|
+
sleep 0.1
|
102
|
+
app.screen.refresh
|
114
103
|
end
|
115
104
|
|
116
|
-
def text_length
|
105
|
+
def text_length(text)
|
117
106
|
Twitter::Validation.tweet_length(text)
|
118
107
|
end
|
119
108
|
|
120
|
-
def
|
109
|
+
def validate!(text)
|
121
110
|
case Twitter::Validation.tweet_invalid?(text)
|
122
111
|
when :empty
|
123
112
|
fail EmptyTextError
|