twterm 2.5.1 → 2.10.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/.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
|