twterm 1.1.3 → 1.2.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/bin/twterm +4 -4
  3. data/lib/twterm/app.rb +19 -4
  4. data/lib/twterm/client.rb +8 -470
  5. data/lib/twterm/direct_message.rb +82 -0
  6. data/lib/twterm/direct_message_composer.rb +74 -0
  7. data/lib/twterm/direct_message_manager.rb +52 -0
  8. data/lib/twterm/event/base.rb +22 -0
  9. data/lib/twterm/event/direct_message/fetched.rb +10 -0
  10. data/lib/twterm/event/favorite.rb +18 -0
  11. data/lib/twterm/event/follow.rb +17 -0
  12. data/lib/twterm/event/notification.rb +33 -0
  13. data/lib/twterm/event/open_uri.rb +11 -0
  14. data/lib/twterm/event/screen/resize.rb +13 -0
  15. data/lib/twterm/event/status/base.rb +14 -0
  16. data/lib/twterm/event/status/delete.rb +13 -0
  17. data/lib/twterm/event/status/mention.rb +10 -0
  18. data/lib/twterm/event/status/timeline.rb +10 -0
  19. data/lib/twterm/event_dispatcher.rb +59 -0
  20. data/lib/twterm/filter_query_window.rb +11 -5
  21. data/lib/twterm/filterable_list.rb +6 -1
  22. data/lib/twterm/notifier.rb +39 -15
  23. data/lib/twterm/promise.rb +2 -2
  24. data/lib/twterm/publisher.rb +16 -0
  25. data/lib/twterm/rest_client.rb +401 -0
  26. data/lib/twterm/screen.rb +16 -13
  27. data/lib/twterm/status.rb +12 -1
  28. data/lib/twterm/streaming_client.rb +103 -0
  29. data/lib/twterm/subscriber.rb +33 -0
  30. data/lib/twterm/tab/base.rb +13 -6
  31. data/lib/twterm/tab/direct_message/conversation.rb +103 -0
  32. data/lib/twterm/tab/direct_message/conversation_list.rb +99 -0
  33. data/lib/twterm/tab/key_assignments_cheatsheet.rb +3 -2
  34. data/lib/twterm/tab/new/list.rb +5 -3
  35. data/lib/twterm/tab/new/search.rb +3 -2
  36. data/lib/twterm/tab/new/start.rb +17 -2
  37. data/lib/twterm/tab/new/user.rb +6 -3
  38. data/lib/twterm/tab/statuses/base.rb +18 -11
  39. data/lib/twterm/tab/statuses/conversation.rb +3 -2
  40. data/lib/twterm/tab/statuses/favorites.rb +3 -2
  41. data/lib/twterm/tab/statuses/home.rb +10 -4
  42. data/lib/twterm/tab/statuses/list_timeline.rb +3 -2
  43. data/lib/twterm/tab/statuses/mentions.rb +6 -6
  44. data/lib/twterm/tab/statuses/search.rb +4 -3
  45. data/lib/twterm/tab/statuses/user_timeline.rb +3 -2
  46. data/lib/twterm/tab/user_tab.rb +26 -16
  47. data/lib/twterm/tab/users/base.rb +3 -2
  48. data/lib/twterm/tab/users/followers.rb +3 -2
  49. data/lib/twterm/tab/users/friends.rb +3 -2
  50. data/lib/twterm/tab_manager.rb +20 -8
  51. data/lib/twterm/tweetbox.rb +5 -2
  52. data/lib/twterm/uri_opener.rb +25 -0
  53. data/lib/twterm/user.rb +11 -1
  54. data/lib/twterm/utils.rb +13 -0
  55. data/lib/twterm/version.rb +1 -1
  56. data/lib/twterm.rb +0 -3
  57. data/spec/twterm/event/screen/resize_spec.rb +11 -0
  58. data/spec/twterm/event_dispatcher_spec.rb +19 -0
  59. data/twterm.gemspec +1 -1
  60. metadata +29 -7
  61. data/lib/twterm/notification/base.rb +0 -24
  62. data/lib/twterm/notification/error.rb +0 -19
  63. data/lib/twterm/notification/message.rb +0 -19
@@ -0,0 +1,103 @@
1
+ require 'twterm/direct_message_composer'
2
+ require 'twterm/event/direct_message/fetched'
3
+ require 'twterm/subscriber'
4
+ require 'twterm/tab/base'
5
+
6
+ module Twterm
7
+ module Tab
8
+ module DirectMessage
9
+ class Conversation < Base
10
+ include FilterableList
11
+ include Scrollable
12
+ include Subscriber
13
+
14
+ def drawable_item_count
15
+ messages.drop(scroller.offset).lazy
16
+ .map { |m| m.text.split_by_width(window.maxx - 4).count + 2 }
17
+ .scan(0, :+)
18
+ .select { |l| l < window.maxy }
19
+ .count
20
+ end
21
+
22
+ def initialize(conversation)
23
+ super()
24
+
25
+ @conversation = conversation
26
+
27
+ subscribe(Event::DirectMessage::Fetched) { refresh }
28
+ end
29
+
30
+ def items
31
+ if filter_query.empty?
32
+ messages
33
+ else
34
+ messages.select { |m| m.matches?(filter_query) }
35
+ end
36
+ end
37
+
38
+ def respond_to_key(key)
39
+ return true if scroller.respond_to_key(key)
40
+
41
+ case key
42
+ when ?/
43
+ filter
44
+ when ?n, ?r
45
+ DirectMessageComposer.instance.compose(conversation.collocutor)
46
+ when ?q
47
+ reset_filter
48
+ else
49
+ return false
50
+ end
51
+
52
+ true
53
+ end
54
+
55
+ def update
56
+ line = 0
57
+
58
+ scroller.drawable_items.each.with_index(0) do |message, i|
59
+ formatted_lines = message.text.split_by_width(window.maxx - 4).count
60
+
61
+ window.with_color(:black, :magenta) do
62
+ formatted_lines.+(1).times do |j|
63
+ window.setpos(line + j, 0)
64
+ window.addch(' ')
65
+ end
66
+ end if scroller.current_item?(i)
67
+
68
+ window.setpos(line, 2)
69
+
70
+ window.bold do
71
+ window.with_color(message.sender.color) do
72
+ window.addstr(message.sender.name)
73
+ end
74
+ end
75
+
76
+ window.addstr(' (@%s)' % message.sender.screen_name)
77
+ window.addstr(' [%s]' % message.date)
78
+
79
+ message.text.split_by_width(window.maxx - 4).each do |str|
80
+ line += 1
81
+ window.setpos(line, 2)
82
+ window.addstr(str)
83
+ end
84
+
85
+ line += 2
86
+ end
87
+ end
88
+
89
+ def title
90
+ '@%s messages' % conversation.collocutor.screen_name
91
+ end
92
+
93
+ private
94
+
95
+ attr_reader :conversation
96
+
97
+ def messages
98
+ @conversation.messages
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,99 @@
1
+ require 'twterm/direct_message_composer'
2
+ require 'twterm/event/direct_message/fetched'
3
+ require 'twterm/subscriber'
4
+ require 'twterm/tab/base'
5
+ require 'twterm/tab/direct_message/conversation'
6
+
7
+ module Twterm
8
+ module Tab
9
+ module DirectMessage
10
+ class ConversationList < Base
11
+ include FilterableList
12
+ include Scrollable
13
+ include Subscriber
14
+
15
+ def drawable_item_count
16
+ window.maxy.-(2).div(3)
17
+ end
18
+
19
+ def initialize
20
+ super
21
+
22
+ subscribe(Event::DirectMessage::Fetched) { refresh }
23
+ end
24
+
25
+ def ==(other)
26
+ other.is_a?(self.class)
27
+ end
28
+
29
+ def items
30
+ if filter_query.empty?
31
+ Client.current.direct_message_conversations
32
+ else
33
+ Client.current.direct_message_conversations.select { |c| c.matches?(filter_query) }
34
+ end
35
+ end
36
+
37
+ def respond_to_key(key)
38
+ return true if scroller.respond_to_key(key)
39
+
40
+ case key
41
+ when 10
42
+ open_conversation
43
+ when ?n, ?r
44
+ conversation = current_item
45
+ DirectMessageComposer.instance.compose(conversation.collocutor)
46
+ when ?/
47
+ filter
48
+ when ?q
49
+ reset_filter
50
+ else
51
+ return false
52
+ end
53
+
54
+ true
55
+ end
56
+
57
+ def update
58
+ scroller.drawable_items.each.with_index(0) do |conversation, i|
59
+ line = 3 * i
60
+
61
+ window.with_color(:black, :magenta) do
62
+ 2.times do |j|
63
+ window.setpos(line + j, 0)
64
+ window.addch(' ')
65
+ end
66
+ end if scroller.current_item?(i)
67
+
68
+ window.setpos(line, 2)
69
+
70
+ window.bold do
71
+ window.with_color(conversation.collocutor.color) do
72
+ window.addstr(conversation.collocutor.name)
73
+ end
74
+ end
75
+
76
+ window.addstr(' (@%s)' % conversation.collocutor.screen_name)
77
+ window.addstr(' [%s]' % conversation.updated_at)
78
+
79
+ window.setpos(line + 1, 2)
80
+ window.addstr(conversation.preview.split_by_width(window.maxx - 4).first)
81
+ end
82
+ end
83
+
84
+ def title
85
+ 'Direct Messages'
86
+ end
87
+
88
+ private
89
+
90
+ def open_conversation
91
+ conversation = scroller.current_item
92
+
93
+ tab = Tab::DirectMessage::Conversation.new(conversation)
94
+ TabManager.instance.add_and_show(tab)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,7 +1,8 @@
1
+ require 'twterm/tab/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
- class KeyAssignmentsCheatsheet
4
- include Base
5
+ class KeyAssignmentsCheatsheet < Base
5
6
  include Scrollable
6
7
 
7
8
  def ==(other)
@@ -1,8 +1,10 @@
1
+ require 'twterm/event/notification'
2
+ require 'twterm/tab/base'
3
+
1
4
  module Twterm
2
5
  module Tab
3
6
  module New
4
- class List
5
- include Base
7
+ class List < Base
6
8
  include FilterableList
7
9
  include Scrollable
8
10
 
@@ -84,7 +86,7 @@ module Twterm
84
86
  window.bold { window.addstr('Open list tab') }
85
87
 
86
88
  Thread.new do
87
- Notifier.instance.show_message('Loading lists ...')
89
+ publish(Event::Notification.new(:message, 'Loading lists ...'))
88
90
  Client.current.lists.then do |lists|
89
91
  @@lists = lists.sort_by(&:full_name)
90
92
  show_lists
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module New
4
- class Search
5
- include Base
6
+ class Search < Base
6
7
  include Readline
7
8
  include FilterableList
8
9
  include Scrollable
@@ -1,8 +1,10 @@
1
+ require 'twterm/tab/base'
2
+ require 'twterm/tab/direct_message/conversation_list'
3
+
1
4
  module Twterm
2
5
  module Tab
3
6
  module New
4
- class Start
5
- include Base
7
+ class Start < Base
6
8
  include Scrollable
7
9
 
8
10
  def ==(other)
@@ -15,6 +17,7 @@ module Twterm
15
17
 
16
18
  def items
17
19
  %i(
20
+ direct_messages
18
21
  list_tab
19
22
  search_tab
20
23
  user_tab
@@ -31,6 +34,8 @@ module Twterm
31
34
  return true if scroller.respond_to_key(key)
32
35
 
33
36
  case key
37
+ when 'D'
38
+ open_direct_messages
34
39
  when 10
35
40
  perform_selected_action
36
41
  when 'L'
@@ -51,6 +56,10 @@ module Twterm
51
56
 
52
57
  private
53
58
 
59
+ def open_direct_messages
60
+ switch(Tab::DirectMessage::ConversationList.new)
61
+ end
62
+
54
63
  def open_list_tab
55
64
  switch(Tab::New::List.new)
56
65
  end
@@ -71,6 +80,8 @@ module Twterm
71
80
 
72
81
  def perform_selected_action
73
82
  case current_item
83
+ when :direct_messages
84
+ open_direct_messages
74
85
  when :list_tab
75
86
  open_list_tab
76
87
  when :search_tab
@@ -95,6 +106,10 @@ module Twterm
95
106
  window.setpos(line, 5)
96
107
 
97
108
  case item
109
+ when :direct_messages
110
+ window.addstr('[D] Direct messages')
111
+ window.setpos(line, 6)
112
+ window.bold { window.addch(?D) }
98
113
  when :list_tab
99
114
  window.addstr('[L] List tab')
100
115
  window.setpos(line, 6)
@@ -1,8 +1,11 @@
1
+ require 'twterm/publisher'
2
+ require 'twterm/tab/base'
3
+
1
4
  module Twterm
2
5
  module Tab
3
6
  module New
4
- class User
5
- include Base
7
+ class User < Base
8
+ include Publisher
6
9
  include Readline
7
10
 
8
11
  def ==(other)
@@ -29,7 +32,7 @@ module Twterm
29
32
  else
30
33
  Client.current.show_user(screen_name).then do |user|
31
34
  if user.nil?
32
- Notifier.instance.show_error 'User not found'
35
+ publish(Event::Notification.new(:error, 'User not found'))
33
36
  tab = Tab::New::Start.new
34
37
  else
35
38
  tab = Tab::UserTab.new(user.id)
@@ -1,14 +1,22 @@
1
+ require 'twterm/event/open_uri'
2
+ require 'twterm/event/status/delete'
3
+ require 'twterm/publisher'
4
+ require 'twterm/subscriber'
5
+ require 'twterm/tab/base'
6
+ require 'twterm/utils'
7
+
1
8
  module Twterm
2
9
  module Tab
3
10
  module Statuses
4
- module Base
5
- include Tab::Base
11
+ class Base < Tab::Base
6
12
  include FilterableList
13
+ include Publisher
7
14
  include Scrollable
15
+ include Subscriber
16
+ include Utils
8
17
 
9
18
  def append(status)
10
- fail ArgumentError,
11
- 'argument must be an instance of Status class' unless status.is_a? Status
19
+ check_type Status, status
12
20
 
13
21
  return if @status_ids.include?(status.id)
14
22
 
@@ -27,10 +35,7 @@ module Twterm
27
35
  def destroy_status
28
36
  status = highlighted_status
29
37
 
30
- Client.current.destroy_status(status).then do
31
- delete(status.id)
32
- refresh
33
- end
38
+ Client.current.destroy_status(status)
34
39
  end
35
40
 
36
41
  def drawable_item_count
@@ -57,6 +62,8 @@ module Twterm
57
62
  super
58
63
 
59
64
  @status_ids = []
65
+
66
+ subscribe(Event::Status::Delete) { |e| delete(e.status_id) }
60
67
  end
61
68
 
62
69
  def items
@@ -68,9 +75,9 @@ module Twterm
68
75
 
69
76
  status = highlighted_status
70
77
  urls = status.urls.map(&:expanded_url) + status.media.map(&:expanded_url)
71
- urls.each(&Launchy.method(:open))
72
- rescue Launchy::CommandNotFoundError
73
- Notifier.instance.show_error 'Cannot find web browser'
78
+ urls
79
+ .map { |url| Event::OpenURI.new(url) }
80
+ .each { |e| publish(e) }
74
81
  end
75
82
 
76
83
  def prepend(status)
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/statuses/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Statuses
4
- class Conversation
5
- include Base
6
+ class Conversation < Base
6
7
  include Dumpable
7
8
 
8
9
  attr_reader :status
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/statuses/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Statuses
4
- class Favorites
5
- include Base
6
+ class Favorites < Base
6
7
  include Dumpable
7
8
 
8
9
  attr_reader :user, :user_id
@@ -1,8 +1,14 @@
1
+ require 'twterm/subscriber'
2
+ require 'twterm/event/status/timeline'
3
+ require 'twterm/tab/statuses/base'
4
+ require 'twterm/utils'
5
+
1
6
  module Twterm
2
7
  module Tab
3
8
  module Statuses
4
- class Home
5
- include Base
9
+ class Home < Base
10
+ include Subscriber
11
+ include Utils
6
12
 
7
13
  def close
8
14
  fail NotClosableError
@@ -17,11 +23,11 @@ module Twterm
17
23
  end
18
24
 
19
25
  def initialize(client)
20
- fail ArgumentError, 'argument must be an instance of Client class' unless client.is_a? Client
26
+ check_type Client, client
21
27
 
22
28
  super()
23
29
  @client = client
24
- @client.on_timeline_status(&method(:prepend))
30
+ subscribe(Event::Status::Timeline) { |e| prepend e.status }
25
31
 
26
32
  fetch { scroller.move_to_top }
27
33
  @auto_reloader = Scheduler.new(180) { fetch }
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/statuses/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Statuses
4
- class ListTimeline
5
- include Base
6
+ class ListTimeline < Base
6
7
  include Dumpable
7
8
 
8
9
  attr_reader :list
@@ -1,8 +1,10 @@
1
+ require 'twterm/tab/statuses/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Statuses
4
- class Mentions
5
- include Base
6
+ class Mentions < Base
7
+ include Subscriber
6
8
 
7
9
  def close
8
10
  fail NotClosableError
@@ -22,10 +24,8 @@ module Twterm
22
24
  super()
23
25
 
24
26
  @client = client
25
- @client.on_mention do |status|
26
- prepend(status)
27
- Notifier.instance.show_message "Mentioned by @#{status.user.screen_name}: #{status.text}"
28
- end
27
+
28
+ subscribe(Event::Status::Mention) { |e| prepend(e.status) }
29
29
 
30
30
  fetch { scroller.move_to_top }
31
31
  @auto_reloader = Scheduler.new(300) { fetch }
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/statuses/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Statuses
4
- class Search
5
- include Base
6
+ class Search < Base
6
7
  include Dumpable
7
8
 
8
9
  attr_reader :query
@@ -22,7 +23,7 @@ module Twterm
22
23
 
23
24
  def fetch
24
25
  Client.current.search(@query).then do |statuses|
25
- statuses.reverse.each(&method(:prepend))
26
+ statuses.each(&method(:append))
26
27
  yield if block_given?
27
28
  end
28
29
  end
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/statuses/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Statuses
4
- class UserTimeline
5
- include Base
6
+ class UserTimeline < Base
6
7
  include Dumpable
7
8
 
8
9
  attr_reader :user, :user_id
@@ -1,8 +1,12 @@
1
+ require 'twterm/event/open_uri'
2
+ require 'twterm/publisher'
3
+ require 'twterm/tab/base'
4
+
1
5
  module Twterm
2
6
  module Tab
3
- class UserTab
4
- include Base
7
+ class UserTab < Base
5
8
  include Dumpable
9
+ include Publisher
6
10
  include Scrollable
7
11
 
8
12
  attr_reader :user_id
@@ -44,6 +48,7 @@ module Twterm
44
48
  show_followers
45
49
  show_likes
46
50
  )
51
+ items << :compose_direct_message unless myself?
47
52
  items << :open_website unless user.website.nil?
48
53
  items << :toggle_follow unless myself?
49
54
  items << :toggle_mute unless myself?
@@ -56,6 +61,8 @@ module Twterm
56
61
  return true if scroller.respond_to_key(key)
57
62
 
58
63
  case key
64
+ when ?D
65
+ compose_direct_message unless myself?
59
66
  when ?F
60
67
  follow unless myself?
61
68
  when 10
@@ -82,8 +89,7 @@ module Twterm
82
89
  refresh
83
90
 
84
91
  user = users.first
85
- msg = "Blocked @#{user.screen_name}"
86
- Notifier.instance.show_message msg
92
+ publish(Event::Notification.new(:message, 'Blocked @%s' % user.screen_name))
87
93
  end
88
94
  end
89
95
 
@@ -91,6 +97,10 @@ module Twterm
91
97
  user.blocked_by?(Client.current.user_id)
92
98
  end
93
99
 
100
+ def compose_direct_message
101
+ DirectMessageComposer.instance.compose(user)
102
+ end
103
+
94
104
  def follow
95
105
  Client.current.follow(user_id).then do |users|
96
106
  refresh
@@ -101,7 +111,7 @@ module Twterm
101
111
  else
102
112
  msg = "Followed @#{user.screen_name}"
103
113
  end
104
- Notifier.instance.show_message msg
114
+ publish(Event::Notification.new(:message, msg))
105
115
  end
106
116
  end
107
117
 
@@ -122,8 +132,7 @@ module Twterm
122
132
  refresh
123
133
 
124
134
  user = users.first
125
- msg = "Muted @#{user.screen_name}"
126
- Notifier.instance.show_message msg
135
+ publish(Event::Notification.new(:message, 'Muted @%s' % user.screen_name))
127
136
  end
128
137
  end
129
138
 
@@ -143,13 +152,13 @@ module Twterm
143
152
  def open_website
144
153
  return if user.website.nil?
145
154
 
146
- Launchy.open(user.website)
147
- rescue Launchy::CommandNotFoundError
148
- Notifier.instance.show_error 'Browser not found'
155
+ publish(Event::OpenURI.new(user.website))
149
156
  end
150
157
 
151
158
  def perform_selected_action
152
159
  case scroller.current_item
160
+ when :compose_direct_message
161
+ compose_direct_message
153
162
  when :open_timeline_tab
154
163
  open_timeline_tab
155
164
  when :open_website
@@ -195,8 +204,7 @@ module Twterm
195
204
  refresh
196
205
 
197
206
  user = users.first
198
- msg = "Unblocked @#{user.screen_name}"
199
- Notifier.instance.show_message msg
207
+ publish(Event::Notification.new(:message, 'Unblocked @%s' % user.screen_name))
200
208
  end
201
209
  end
202
210
 
@@ -205,8 +213,7 @@ module Twterm
205
213
  refresh
206
214
 
207
215
  user = users.first
208
- msg = "Unfollowed @#{user.screen_name}"
209
- Notifier.instance.show_message msg
216
+ publish(Event::Notification.new(:message, 'Unfollowed @%s' % user.screen_name))
210
217
  end
211
218
  end
212
219
 
@@ -215,8 +222,7 @@ module Twterm
215
222
  refresh
216
223
 
217
224
  user = users.first
218
- msg = "Unmuted @#{user.screen_name}"
219
- Notifier.instance.show_message msg
225
+ publish(Event::Notification.new(:message, 'Unmuted @%s' % user.screen_name))
220
226
  end
221
227
  end
222
228
 
@@ -263,6 +269,10 @@ module Twterm
263
269
 
264
270
  window.setpos(current_line, 5)
265
271
  case item
272
+ when :compose_direct_message
273
+ window.addstr('[ ] Compose direct message')
274
+ window.setpos(current_line, 6)
275
+ window.bold { window.addch(?D) }
266
276
  when :toggle_block
267
277
  if blocking?
268
278
  window.addstr(' Unblock this user')
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Users
4
- module Base
5
- include Tab::Base
6
+ class Base < Tab::Base
6
7
  include FilterableList
7
8
  include Scrollable
8
9
 
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/users/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Users
4
- class Followers
5
- include Base
6
+ class Followers < Base
6
7
  include Dumpable
7
8
 
8
9
  attr_reader :user_id
@@ -1,8 +1,9 @@
1
+ require 'twterm/tab/users/base'
2
+
1
3
  module Twterm
2
4
  module Tab
3
5
  module Users
4
- class Friends
5
- include Base
6
+ class Friends < Base
6
7
  include Dumpable
7
8
 
8
9
  attr_reader :user_id