twterm 2.5.1 → 2.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +62 -0
- data/.gitignore +1 -0
- data/Makefile +21 -0
- data/README.md +52 -6
- data/bin/twterm +1 -3
- data/default.nix +21 -0
- data/gemset.nix +4981 -0
- data/lib/twterm/app.rb +13 -26
- data/lib/twterm/client.rb +1 -6
- data/lib/twterm/color_manager.rb +8 -9
- data/lib/twterm/image.rb +31 -0
- data/lib/twterm/image/attr.rb +42 -0
- data/lib/twterm/image/bold.rb +7 -21
- data/lib/twterm/image/color.rb +9 -15
- data/lib/twterm/image/dim.rb +13 -0
- data/lib/twterm/image/underlined.rb +17 -0
- data/lib/twterm/image_builder/user_name_image_builder.rb +1 -0
- data/lib/twterm/key_mapper.rb +6 -8
- data/lib/twterm/key_mapper/abstract_key_mapper.rb +2 -2
- data/lib/twterm/list.rb +36 -1
- data/lib/twterm/message_window.rb +4 -13
- data/lib/twterm/persistable_configuration_proxy.rb +6 -6
- data/lib/twterm/preferences.rb +8 -1
- data/lib/twterm/rest_client.rb +0 -33
- data/lib/twterm/screen.rb +103 -25
- data/lib/twterm/search_query_window.rb +5 -13
- data/lib/twterm/status.rb +10 -0
- data/lib/twterm/tab/abstract_tab.rb +30 -16
- data/lib/twterm/tab/new/index.rb +0 -10
- data/lib/twterm/tab/new/search.rb +2 -2
- data/lib/twterm/tab/new/user.rb +2 -2
- data/lib/twterm/tab/preferences/control.rb +77 -0
- data/lib/twterm/tab/preferences/index.rb +6 -0
- data/lib/twterm/tab/scrollable.rb +1 -1
- data/lib/twterm/tab/searchable.rb +6 -4
- data/lib/twterm/tab/status_tab.rb +10 -0
- data/lib/twterm/tab/statuses/abstract_statuses_tab.rb +11 -6
- data/lib/twterm/tab/statuses/home.rb +0 -3
- data/lib/twterm/tab/statuses/mentions.rb +1 -0
- data/lib/twterm/tab/user_tab.rb +0 -9
- data/lib/twterm/tab_manager.rb +77 -10
- data/lib/twterm/tweetbox.rb +2 -3
- data/lib/twterm/user.rb +1 -0
- data/lib/twterm/version.rb +1 -1
- data/nix/Gemfile +3 -0
- data/nix/Gemfile.lock +77 -0
- data/nix/gemset.nix +325 -0
- data/shell.nix +40 -0
- data/spec/twterm/image/bold_spec.rb +30 -0
- data/spec/twterm/image/color_spec.rb +16 -0
- data/spec/twterm/image/dim_spec.rb +30 -0
- data/twterm.gemspec +13 -13
- metadata +46 -46
- data/.travis.yml +0 -12
- data/lib/twterm/direct_message.rb +0 -60
- data/lib/twterm/direct_message_composer.rb +0 -80
- data/lib/twterm/direct_message_manager.rb +0 -51
- data/lib/twterm/event/direct_message/fetched.rb +0 -10
- data/lib/twterm/event/notification/direct_message.rb +0 -30
- data/lib/twterm/event/screen/resize.rb +0 -13
- data/lib/twterm/event/status/timeline.rb +0 -10
- data/lib/twterm/repository/direct_message_repository.rb +0 -14
- data/lib/twterm/streaming_client.rb +0 -146
- data/lib/twterm/tab/direct_message/conversation.rb +0 -104
- data/lib/twterm/tab/direct_message/conversation_list.rb +0 -100
- data/spec/twterm/event/screen/resize_spec.rb +0 -11
data/.travis.yml
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
require 'twterm/user'
|
2
|
-
require 'twterm/utils'
|
3
|
-
|
4
|
-
module Twterm
|
5
|
-
class DirectMessage
|
6
|
-
attr_reader :id, :created_at, :recipient_id, :sender_id, :text
|
7
|
-
|
8
|
-
def initialize(message)
|
9
|
-
@id = message.id
|
10
|
-
update!(message)
|
11
|
-
end
|
12
|
-
|
13
|
-
def ==(other)
|
14
|
-
other.is_a?(self.class) && id == other.id
|
15
|
-
end
|
16
|
-
|
17
|
-
def date
|
18
|
-
format = Time.now - @created_at < 86_400 ? '%H:%M:%S' : '%Y-%m-%d %H:%M:%S'
|
19
|
-
@created_at.strftime(format)
|
20
|
-
end
|
21
|
-
|
22
|
-
def update!(message)
|
23
|
-
@created_at = message.created_at.dup.localtime
|
24
|
-
@recipient_id = message.recipient.id
|
25
|
-
@sender_id = message.sender.id
|
26
|
-
@text = message.text
|
27
|
-
|
28
|
-
self
|
29
|
-
end
|
30
|
-
|
31
|
-
class Conversation
|
32
|
-
include Utils
|
33
|
-
|
34
|
-
attr_reader :collocutor_id, :messages
|
35
|
-
|
36
|
-
def initialize(collocutor_id)
|
37
|
-
@collocutor_id = collocutor_id
|
38
|
-
@messages = []
|
39
|
-
end
|
40
|
-
|
41
|
-
def <<(message)
|
42
|
-
@messages << message if messages.find { |m| m == message }.nil?
|
43
|
-
@messages.sort_by!(&:created_at).reverse!
|
44
|
-
|
45
|
-
self
|
46
|
-
end
|
47
|
-
|
48
|
-
def preview
|
49
|
-
messages.sort_by(&:created_at).last.text.gsub("\n", ' ')
|
50
|
-
end
|
51
|
-
|
52
|
-
def updated_at
|
53
|
-
updated_at = @messages.map(&:created_at).max
|
54
|
-
|
55
|
-
format = Time.now - updated_at < 86_400 ? '%H:%M:%S' : '%Y-%m-%d %H:%M:%S'
|
56
|
-
updated_at.strftime(format)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
@@ -1,80 +0,0 @@
|
|
1
|
-
require 'twterm/event/screen/refresh'
|
2
|
-
require 'twterm/publisher'
|
3
|
-
require 'twterm/user'
|
4
|
-
require 'twterm/utils'
|
5
|
-
|
6
|
-
module Twterm
|
7
|
-
class DirectMessageComposer
|
8
|
-
include Readline
|
9
|
-
include Curses
|
10
|
-
include Publisher
|
11
|
-
include Utils
|
12
|
-
|
13
|
-
def initialize(app, client)
|
14
|
-
@app, @client = app, client
|
15
|
-
end
|
16
|
-
|
17
|
-
def compose(recipient)
|
18
|
-
check_type User, recipient
|
19
|
-
|
20
|
-
clear
|
21
|
-
|
22
|
-
resetter = proc do
|
23
|
-
reset_prog_mode
|
24
|
-
sleep 0.1
|
25
|
-
publish(Event::Screen::Refresh.new)
|
26
|
-
end
|
27
|
-
|
28
|
-
thread = Thread.new do
|
29
|
-
close_screen
|
30
|
-
|
31
|
-
puts "\nCompose new message to @%s:" % recipient.screen_name
|
32
|
-
|
33
|
-
app.completion_manager.set_default_mode!
|
34
|
-
|
35
|
-
loop do
|
36
|
-
line = (readline('> ', true) || '').strip
|
37
|
-
break if line.empty?
|
38
|
-
|
39
|
-
if line.end_with?('\\')
|
40
|
-
@text << line.chop.rstrip + "\n"
|
41
|
-
else
|
42
|
-
@text << line
|
43
|
-
break
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
puts "\n"
|
48
|
-
|
49
|
-
resetter.call
|
50
|
-
send(recipient) unless text.empty?
|
51
|
-
end
|
52
|
-
|
53
|
-
app.register_interruption_handler do
|
54
|
-
thread.kill
|
55
|
-
clear
|
56
|
-
puts "\nCanceled"
|
57
|
-
resetter.call
|
58
|
-
end
|
59
|
-
|
60
|
-
thread.join
|
61
|
-
end
|
62
|
-
|
63
|
-
private
|
64
|
-
|
65
|
-
attr_reader :app, :client
|
66
|
-
|
67
|
-
def clear
|
68
|
-
@text = ''
|
69
|
-
end
|
70
|
-
|
71
|
-
def send(recipient)
|
72
|
-
client.create_direct_message(recipient, text)
|
73
|
-
clear
|
74
|
-
end
|
75
|
-
|
76
|
-
def text
|
77
|
-
@text || ''
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
require 'twterm/client'
|
2
|
-
require 'twterm/direct_message'
|
3
|
-
require 'twterm/event/direct_message/fetched'
|
4
|
-
require 'twterm/publisher'
|
5
|
-
require 'twterm/scheduler'
|
6
|
-
require 'twterm/user'
|
7
|
-
require 'twterm/utils'
|
8
|
-
|
9
|
-
module Twterm
|
10
|
-
class DirectMessageManager
|
11
|
-
include Publisher, Utils
|
12
|
-
|
13
|
-
def initialize(client)
|
14
|
-
check_type Client, client
|
15
|
-
|
16
|
-
@client = client
|
17
|
-
@conversations = {}
|
18
|
-
|
19
|
-
fetch
|
20
|
-
|
21
|
-
Scheduler.new(300) { fetch }
|
22
|
-
end
|
23
|
-
|
24
|
-
def add(collocutor_id, message)
|
25
|
-
check_type DirectMessage, message
|
26
|
-
|
27
|
-
@conversations[collocutor_id] ||= DirectMessage::Conversation.new(collocutor_id)
|
28
|
-
@conversations[collocutor_id] << message
|
29
|
-
end
|
30
|
-
|
31
|
-
def fetch
|
32
|
-
client.direct_messages_received.then do |messages|
|
33
|
-
messages.each { |m| add(m.sender_id, m) }
|
34
|
-
publish(Event::DirectMessage::Fetched.new)
|
35
|
-
end
|
36
|
-
|
37
|
-
client.direct_messages_sent.then do |messages|
|
38
|
-
messages.each { |m| add(m.recipient_id, m) }
|
39
|
-
publish(Event::DirectMessage::Fetched.new)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def conversations
|
44
|
-
@conversations.values
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
attr_reader :client
|
50
|
-
end
|
51
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
require 'twterm/event/notification/abstract_notification'
|
2
|
-
|
3
|
-
module Twterm
|
4
|
-
module Event
|
5
|
-
module Notification
|
6
|
-
class DirectMessage < AbstractNotification
|
7
|
-
# @param [Twterm::DirectMessage] message
|
8
|
-
# @param [Twterm::User] user
|
9
|
-
def initialize(message, user)
|
10
|
-
@message = message
|
11
|
-
@user = user
|
12
|
-
end
|
13
|
-
|
14
|
-
# @return [String] notification body
|
15
|
-
def body
|
16
|
-
message.text
|
17
|
-
end
|
18
|
-
|
19
|
-
# @return [String] notification title
|
20
|
-
def title
|
21
|
-
"@#{user.screen_name} has sent you a message"
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
attr_reader :message, :user
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
require 'twterm/direct_message'
|
2
|
-
require 'twterm/repository/abstract_entity_repository'
|
3
|
-
|
4
|
-
module Twterm
|
5
|
-
module Repository
|
6
|
-
class DirectMessageRepository < AbstractEntityRepository
|
7
|
-
private
|
8
|
-
|
9
|
-
def type
|
10
|
-
DirectMessage
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,146 +0,0 @@
|
|
1
|
-
require 'twterm/event/message/error'
|
2
|
-
require 'twterm/event/message/info'
|
3
|
-
require 'twterm/event/notification/direct_message'
|
4
|
-
require 'twterm/event/notification/favorite'
|
5
|
-
require 'twterm/event/notification/follow'
|
6
|
-
require 'twterm/event/notification/list_member_added'
|
7
|
-
require 'twterm/event/notification/mention'
|
8
|
-
require 'twterm/event/notification/quote'
|
9
|
-
require 'twterm/event/notification/retweet'
|
10
|
-
require 'twterm/event/status/mention'
|
11
|
-
require 'twterm/event/status/timeline'
|
12
|
-
require 'twterm/publisher'
|
13
|
-
|
14
|
-
module Twterm
|
15
|
-
module StreamingClient
|
16
|
-
include Publisher
|
17
|
-
|
18
|
-
CONSUMER_KEY = 'vLNSVFgXclBJQJRZ7VLMxL9lA'.freeze
|
19
|
-
CONSUMER_SECRET = 'OFLKzrepRG2p1hq0nUB9j2S9ndFQoNTPheTpmOY0GYw55jGgS5'.freeze
|
20
|
-
|
21
|
-
def connect_user_stream
|
22
|
-
return if user_stream_connected?
|
23
|
-
|
24
|
-
@streaming_thread = Thread.new do
|
25
|
-
begin
|
26
|
-
publish(Event::Message::Info.new('Trying to connect to Twitter...'))
|
27
|
-
streaming_client.user do |event|
|
28
|
-
keep_alive!
|
29
|
-
|
30
|
-
case event
|
31
|
-
when Twitter::Tweet
|
32
|
-
status = status_repository.create(event)
|
33
|
-
user = user_repository.create(event.user)
|
34
|
-
|
35
|
-
publish(Event::Status::Timeline.new(status))
|
36
|
-
|
37
|
-
if !status.retweet? && status.text.include?('@%s' % screen_name)
|
38
|
-
publish(Event::Status::Mention.new(status))
|
39
|
-
|
40
|
-
notification = Event::Notification::Mention.new(status, user)
|
41
|
-
publish(notification)
|
42
|
-
end
|
43
|
-
|
44
|
-
if status.retweet? && event.retweeted_status.user.id == user_id
|
45
|
-
retweeted_status = status_repository.create(event.retweeted_status)
|
46
|
-
notification = Event::Notification::Retweet.new(retweeted_status, user)
|
47
|
-
publish(notification)
|
48
|
-
end
|
49
|
-
when Twitter::Streaming::Event
|
50
|
-
case event.name
|
51
|
-
when :favorite
|
52
|
-
next if event.source.id == user_id
|
53
|
-
user = user_repository.create(event.source)
|
54
|
-
status = status_repository.create(event.target_object)
|
55
|
-
notification = Event::Notification::Like.new(status, user)
|
56
|
-
publish(notification)
|
57
|
-
when :follow
|
58
|
-
next if event.source.id == user_id
|
59
|
-
|
60
|
-
user = user_repository.create(event.source)
|
61
|
-
notification = Event::Notification::Follow.new(user)
|
62
|
-
publish(notification)
|
63
|
-
when :list_member_added
|
64
|
-
next if event.source.id == user_id
|
65
|
-
|
66
|
-
list = list_repository.create(event.target_object)
|
67
|
-
notification = Event::Notification::ListMemberAdded.new(list)
|
68
|
-
publish(notification)
|
69
|
-
when :quoted_tweet
|
70
|
-
next if event.source.id == user_id
|
71
|
-
|
72
|
-
status = status_repository.create(event.target_object)
|
73
|
-
user = user_repository.create(event.source)
|
74
|
-
notification = Event::Notification::Quote.new(status, user)
|
75
|
-
publish(notification)
|
76
|
-
end
|
77
|
-
when Twitter::DirectMessage
|
78
|
-
next if event.sender.id == user_id
|
79
|
-
|
80
|
-
user = user_repository.create(event.sender)
|
81
|
-
message = DirectMessage.new(event)
|
82
|
-
notification = Event::Notification::DirectMessage.new(message, user)
|
83
|
-
publish(notification)
|
84
|
-
when Twitter::Streaming::FriendList
|
85
|
-
user_stream_connected!
|
86
|
-
when Twitter::Streaming::DeletedTweet
|
87
|
-
publish(Event::StatusDeleted.new(event.id))
|
88
|
-
end
|
89
|
-
end
|
90
|
-
rescue Twitter::Error::TooManyRequests
|
91
|
-
publish(Event::Message::Error.new('Rate limit exceeded'))
|
92
|
-
sleep 120
|
93
|
-
retry
|
94
|
-
rescue Errno::ENETUNREACH, Errno::ETIMEDOUT, Resolv::ResolvError
|
95
|
-
publish(Event::Message::Error.new('Network is unavailable'))
|
96
|
-
sleep 30
|
97
|
-
retry
|
98
|
-
rescue Twitter::Error => e
|
99
|
-
publish(Event::Message::Error.new(e.message))
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
private
|
105
|
-
|
106
|
-
def keep_alive!
|
107
|
-
@keep_alive_timer.kill if @keep_alive_timer.is_a?(Thread)
|
108
|
-
@keep_alive_timer = Thread.new do
|
109
|
-
sleep(120)
|
110
|
-
@user_stream_connected = false
|
111
|
-
@streaming_thread.kill
|
112
|
-
connect_user_stream
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def streaming_client
|
117
|
-
@streaming_client ||= Twitter::Streaming::Client.new do |config|
|
118
|
-
config.consumer_key = CONSUMER_KEY
|
119
|
-
config.consumer_secret = CONSUMER_SECRET
|
120
|
-
config.access_token = @access_token
|
121
|
-
config.access_token_secret = @access_token_secret
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
def user_stream_connected?
|
126
|
-
@user_stream_connected || false
|
127
|
-
end
|
128
|
-
|
129
|
-
def user_stream_connected!
|
130
|
-
publish(Event::Message::Info.new('Connection established')) unless user_stream_connected?
|
131
|
-
@user_stream_connected = true
|
132
|
-
end
|
133
|
-
|
134
|
-
def user_stream_disconnected!
|
135
|
-
@user_stream_connected = false
|
136
|
-
end
|
137
|
-
|
138
|
-
def user_stream_initialized?
|
139
|
-
@user_stream_initialized || false
|
140
|
-
end
|
141
|
-
|
142
|
-
def user_stream_initialized!
|
143
|
-
@user_stream_initialized = true
|
144
|
-
end
|
145
|
-
end
|
146
|
-
end
|
@@ -1,104 +0,0 @@
|
|
1
|
-
require 'twterm/direct_message_composer'
|
2
|
-
require 'twterm/event/direct_message/fetched'
|
3
|
-
require 'twterm/image_builder/user_name_image_builder'
|
4
|
-
require 'twterm/subscriber'
|
5
|
-
require 'twterm/tab/abstract_tab'
|
6
|
-
require 'twterm/tab/loadable'
|
7
|
-
require 'twterm/tab/searchable'
|
8
|
-
|
9
|
-
module Twterm
|
10
|
-
module Tab
|
11
|
-
module DirectMessage
|
12
|
-
class Conversation < AbstractTab
|
13
|
-
include Searchable
|
14
|
-
include Subscriber
|
15
|
-
include Loadable
|
16
|
-
|
17
|
-
def drawable_item_count
|
18
|
-
messages.drop(scroller.offset).lazy
|
19
|
-
.map { |m| m.text.split_by_width(window.maxx - 4).count + 2 }
|
20
|
-
.scan(0, :+)
|
21
|
-
.each_cons(2)
|
22
|
-
.select { |_, l| l < window.maxy }
|
23
|
-
.count
|
24
|
-
end
|
25
|
-
|
26
|
-
def image
|
27
|
-
return Image.string(initially_loaded? ? 'No results found' : 'Loading...') if items.empty?
|
28
|
-
|
29
|
-
scroller.drawable_items.map.with_index(0) do |message, i|
|
30
|
-
sender = app.user_repository.find(message.sender_id)
|
31
|
-
|
32
|
-
header = [
|
33
|
-
ImageBuilder::UserNameImageBuilder.new(sender).build,
|
34
|
-
Image.string(message.date.to_s).brackets,
|
35
|
-
].intersperse(Image.whitespace).reduce(Image.empty, :-)
|
36
|
-
|
37
|
-
body = message.text.split_by_width(window.maxx - 4)
|
38
|
-
.map { |x| Image.string(x) }
|
39
|
-
.reduce(Image.empty, :|)
|
40
|
-
|
41
|
-
m = header | body
|
42
|
-
|
43
|
-
cursor = Image.cursor(m.height, scroller.current_index?(i))
|
44
|
-
|
45
|
-
cursor - Image.whitespace - m
|
46
|
-
end
|
47
|
-
.intersperse(Image.blank_line)
|
48
|
-
.reduce(Image.empty, :|)
|
49
|
-
end
|
50
|
-
|
51
|
-
def initialize(app, client, conversation)
|
52
|
-
super(app, client)
|
53
|
-
|
54
|
-
@conversation = conversation
|
55
|
-
|
56
|
-
subscribe(Event::DirectMessage::Fetched) { initially_loaded! }
|
57
|
-
end
|
58
|
-
|
59
|
-
def items
|
60
|
-
messages
|
61
|
-
end
|
62
|
-
|
63
|
-
def matches?(message, query)
|
64
|
-
sender = app.user_repository.find(message.sender_id)
|
65
|
-
|
66
|
-
[
|
67
|
-
message.text,
|
68
|
-
sender.screen_name,
|
69
|
-
sender.name
|
70
|
-
].any? { |x| x.downcase.include?(query.downcase) }
|
71
|
-
end
|
72
|
-
|
73
|
-
def respond_to_key(key)
|
74
|
-
return true if scroller.respond_to_key(key)
|
75
|
-
|
76
|
-
k = KeyMapper.instance
|
77
|
-
|
78
|
-
case key
|
79
|
-
when k[:status, :compose], k[:status, :reply]
|
80
|
-
collocutor = app.user_repository.find(conversation.collocutor_id)
|
81
|
-
app.direct_message_composer.compose(collocutor)
|
82
|
-
else
|
83
|
-
return false
|
84
|
-
end
|
85
|
-
|
86
|
-
true
|
87
|
-
end
|
88
|
-
|
89
|
-
def title
|
90
|
-
collocutor = app.user_repository.find(conversation.collocutor_id)
|
91
|
-
'@%s messages' % collocutor.screen_name
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
attr_reader :conversation
|
97
|
-
|
98
|
-
def messages
|
99
|
-
@conversation.messages
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|