usaidwat 1.1.1 → 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.
@@ -0,0 +1,33 @@
1
+ Feature: Display user information
2
+
3
+ As a Redditor
4
+ I want to be able to list basic information about another Redditor
5
+ In order to learn more about them
6
+
7
+ Scenario: List user information
8
+ Given the Reddit service returns information for the user "mipadi"
9
+ And time is frozen at Sep 15, 2015 3:16 PM
10
+ When I run `usaidwat info mipadi`
11
+ Then the exit status should be 0
12
+ And the output should match:
13
+ """
14
+ Created: [A-Z][a-z]{2} \d{2}, \d{4} \d{2}:\d{2} (A|P)M \(over 7 years ago\)
15
+ Link Karma: 4892
16
+ Comment Karma: 33440
17
+ """
18
+
19
+ Scenario: List user information for a non-existent user
20
+ Given the Reddit service does not have a user "testuser"
21
+ When I run `usaidwat info testuser`
22
+ Then it should fail with:
23
+ """
24
+ No such user: testuser
25
+ """
26
+
27
+ Scenario: Fail to pass a username when querying for information
28
+ Given the Reddit service returns information for the user "mipadi"
29
+ When I run `usaidwat info`
30
+ Then it should fail with:
31
+ """
32
+ You must specify a username
33
+ """
@@ -1,5 +1,6 @@
1
1
  require 'usaidwat/algo'
2
2
  require 'usaidwat/client'
3
+ require 'usaidwat/either'
3
4
  require 'usaidwat/pager'
4
5
  require 'sysexits'
5
6
 
@@ -38,11 +39,37 @@ module USaidWat
38
39
  end
39
40
  end
40
41
 
42
+ class Info < Command
43
+ def initialize(prog)
44
+ prog.command(:info) do |c|
45
+ c.action do |args, options|
46
+ process(options, args)
47
+ end
48
+ end
49
+ super
50
+ end
51
+
52
+ def process(options, args)
53
+ raise ArgumentError.new('You must specify a username') if args.empty?
54
+ username = args.shift
55
+
56
+ redditor = client.new(username)
57
+ created_at = redditor.created_at.strftime("%b %d, %Y %H:%M %p")
58
+ puts "Created: #{created_at} (#{redditor.age})"
59
+ printf "Link Karma: %d\n", redditor.link_karma
60
+ printf "Comment Karma: %d\n", redditor.comment_karma
61
+ rescue USaidWat::Client::NoSuchUserError
62
+ quit "No such user: #{username}", :no_such_user
63
+ end
64
+ end
65
+
41
66
  class Log < Command
42
67
  def initialize(prog)
43
68
  prog.command(:log) do |c|
44
69
  c.alias :l
70
+ c.option 'date', '--date FORMAT', 'Show dates in "absolute" or "relative" format'
45
71
  c.option 'grep', '--grep STRING', 'Show only comments matching STRING'
72
+ c.option 'limit', '-n LIMIT', 'Only show n comments'
46
73
  c.option 'oneline', '--oneline', 'Output log in a more compact form'
47
74
  c.option 'raw', '--raw', 'Print raw comment bodies'
48
75
 
@@ -56,31 +83,69 @@ module USaidWat
56
83
  def process(options, args)
57
84
  raise ArgumentError.new('You must specify a username') if args.empty?
58
85
  username = args.shift
59
- subreddit = args.shift
86
+ subreddits = args.join(' ').split(/[ ,\+]/).map { |sr| sr.downcase }
60
87
 
61
88
  redditor = client.new(username)
62
89
  comments = redditor.comments
63
- if subreddit
64
- comments = comments.group_by { |c| c.subreddit.downcase }
65
- comments = comments[subreddit.downcase]
66
- quit "No comments by #{redditor.username} for #{subreddit}." if comments.nil?
67
- end
68
- comments = comments.select { |c| c.body =~ /#{options['grep']}/i } if options['grep']
69
- if comments.empty?
70
- msg = "#{redditor.username} has no comments"
71
- msg = "#{msg} matching /#{options['grep']}/" if options['grep']
72
- msg = "#{msg}."
73
- quit msg
74
- end
75
- list_comments(comments, options['grep'], !options['oneline'].nil?, !options['raw'].nil?)
90
+
91
+ res = filter_comments(redditor, comments, subreddits) >>
92
+ lambda { |r| grep_comments(redditor, r.value, options['grep']) } >>
93
+ lambda { |r| limit_comments(redditor, r.value, options['limit']) } >>
94
+ lambda { |r| ensure_comments(redditor, r.value) }
95
+
96
+ quit res.value if res.left?
97
+
98
+ opts = {
99
+ :date_format => (options['date'] || :relative).to_sym,
100
+ :oneline => !options['oneline'].nil?,
101
+ :pattern => options['grep'],
102
+ :raw => !options['raw'].nil?,
103
+ }
104
+ list_comments(res.value, opts)
76
105
  rescue USaidWat::Client::NoSuchUserError
77
106
  quit "No such user: #{username}", :no_such_user
78
107
  end
79
108
 
80
109
  private
81
110
 
82
- def list_comments(comments, pattern = nil, oneline = false, raw = false)
83
- formatter = (oneline ? USaidWat::CLI::CompactCommentFormatter : USaidWat::CLI::CommentFormatter).new(pattern, raw)
111
+ def filter_comments(redditor, comments, subreddits)
112
+ return USaidWat::Right.new(comments) if subreddits.empty?
113
+ comments = comments.find_all { |c| subreddits.include?(c.subreddit.downcase) }
114
+ if comments.empty?
115
+ USaidWat::Left.new("No comments by #{redditor.username} for #{subreddits.join(', ')}.")
116
+ else
117
+ USaidWat::Right.new(comments)
118
+ end
119
+ end
120
+
121
+ def grep_comments(redditor, comments, grep)
122
+ return USaidWat::Right.new(comments) if grep.nil?
123
+ comments = comments.select { |c| c.body =~ /#{grep}/i }
124
+ if comments.empty?
125
+ msg = "#{redditor.username} has no comments matching /#{grep}/."
126
+ USaidWat::Left.new(msg)
127
+ else
128
+ USaidWat::Right.new(comments)
129
+ end
130
+ end
131
+
132
+ def limit_comments(redditor, comments, n)
133
+ return USaidWat::Right.new(comments) if n.nil?
134
+ comments = comments[0...n.to_i]
135
+ USaidWat::Right.new(comments)
136
+ end
137
+
138
+ def ensure_comments(redditor, comments)
139
+ if comments.empty?
140
+ USaidWat::Left.new("#{redditor.username} has no comments.")
141
+ else
142
+ USaidWat::Right.new(comments)
143
+ end
144
+ end
145
+
146
+ def list_comments(comments, options = {})
147
+ oneline = options[:oneline]
148
+ formatter = (oneline ? USaidWat::CLI::CompactCommentFormatter : USaidWat::CLI::CommentFormatter).new(options)
84
149
  page
85
150
  comments.each { |c| print formatter.format(c) }
86
151
  end
@@ -105,7 +170,6 @@ module USaidWat
105
170
  username = args.first
106
171
 
107
172
  redditor = client.new(username)
108
- algo_cls = options['count'] ? USaidWat::Algorithms::CountAlgorithm : USaidWat::Algorithms::LexicographicalAlgorithm
109
173
  quit "#{redditor.username} has no comments." if redditor.comments.empty?
110
174
  # Unfortunately Snooby cannot return comments for a specific
111
175
  # user in a specific subreddit, so for now we have to sort them
@@ -117,7 +181,7 @@ module USaidWat
117
181
  longest_subreddit = subreddit.length if subreddit.length > longest_subreddit
118
182
  buckets[subreddit] += 1
119
183
  end
120
- algo = algo_cls.new(buckets)
184
+ algo = algorithm(options['count']).new(buckets)
121
185
  subreddits = buckets.keys.sort { |a,b| algo.sort(a, b) }
122
186
  subreddits.each do |subreddit|
123
187
  tally = buckets[subreddit]
@@ -126,6 +190,12 @@ module USaidWat
126
190
  rescue USaidWat::Client::NoSuchUserError
127
191
  quit "No such user: #{username}", :no_such_user
128
192
  end
193
+
194
+ private
195
+
196
+ def algorithm(count)
197
+ count ? USaidWat::Algorithms::CountAlgorithm : USaidWat::Algorithms::LexicographicalAlgorithm
198
+ end
129
199
  end
130
200
  end
131
201
  end
@@ -1,5 +1,6 @@
1
1
  require 'snooby'
2
2
  require 'usaidwat/service'
3
+ require 'usaidwat/ext/time'
3
4
 
4
5
  module USaidWat
5
6
  module Client
@@ -15,16 +16,46 @@ module USaidWat
15
16
  end
16
17
 
17
18
  def comments
18
- @service.user(username).comments(100)
19
+ user.comments(100)
19
20
  rescue NoMethodError
20
21
  raise NoSuchUserError, username
21
22
  rescue TypeError
22
23
  raise ReachabilityError, "Reddit unreachable"
23
24
  end
24
25
 
26
+ def link_karma
27
+ about('link_karma')
28
+ end
29
+
30
+ def comment_karma
31
+ about('comment_karma')
32
+ end
33
+
34
+ def created_at
35
+ Time.at(about('created_utc'))
36
+ end
37
+
38
+ def age
39
+ (Time.now - created_at).ago
40
+ end
41
+
25
42
  def to_s
26
43
  "#{username}"
27
44
  end
45
+
46
+ private
47
+
48
+ def user
49
+ @service.user(username)
50
+ end
51
+
52
+ def about(key)
53
+ user.about[key]
54
+ rescue NoMethodError
55
+ raise NoSuchUserError, username
56
+ rescue TypeError
57
+ raise ReachabilityError, "Reddit unreachable"
58
+ end
28
59
  end
29
60
 
30
61
  class Redditor < BaseRedditor
@@ -0,0 +1,41 @@
1
+ module USaidWat
2
+ class Either
3
+ attr_reader :value
4
+
5
+ def initialize(value)
6
+ @value = value
7
+ end
8
+
9
+ def >>(&block)
10
+ raise NoMethodError, 'subclasses must define >>'
11
+ end
12
+
13
+ def left?
14
+ false
15
+ end
16
+
17
+ def right?
18
+ false
19
+ end
20
+ end
21
+
22
+ class Left < Either
23
+ def >>(callable)
24
+ self
25
+ end
26
+
27
+ def left?
28
+ true
29
+ end
30
+ end
31
+
32
+ class Right < Either
33
+ def >>(callable)
34
+ callable.call(self)
35
+ end
36
+
37
+ def right?
38
+ true
39
+ end
40
+ end
41
+ end
@@ -2,9 +2,60 @@ require 'usaidwat/ext/string'
2
2
 
3
3
  class Time
4
4
  def ago
5
- case minutes_ago
5
+ delta.ago
6
+ end
7
+
8
+ def seconds_ago
9
+ delta.seconds_ago
10
+ end
11
+
12
+ def minutes_ago
13
+ delta.minutes_ago
14
+ end
15
+
16
+ def hours_ago
17
+ delta.hours_ago
18
+ end
19
+
20
+ def days_ago
21
+ delta.days_ago
22
+ end
23
+
24
+ def weeks_ago
25
+ delta.weeks_ago
26
+ end
27
+
28
+ def months_ago
29
+ delta.months_ago
30
+ end
31
+
32
+ def years_ago
33
+ delta.years_ago
34
+ end
35
+
36
+ private
37
+
38
+ def delta
39
+ Time.now - self
40
+ end
41
+ end
42
+
43
+ class Numeric
44
+ def negative?
45
+ self < 0
46
+ end
47
+
48
+ def positive?
49
+ self > 0
50
+ end
51
+ end
52
+
53
+ class Float
54
+ def ago
55
+ raise ArgumentError, "Delta is negative: #{self}" if negative?
56
+ case minutes_ago.to_i
6
57
  when 0..1
7
- case seconds_ago
58
+ case seconds_ago.to_i
8
59
  when 0..5 then "less than 5 seconds ago"
9
60
  when 6..10 then "less than 10 seconds ago"
10
61
  when 11..20 then "less than 20 seconds ago"
@@ -20,12 +71,12 @@ class Time
20
71
  when 10081..43220 then "about #{weeks_ago.round} #{"week".pluralize(weeks_ago.round)} ago"
21
72
  when 43221..525960 then "about #{months_ago.round} #{"month".pluralize(months_ago.round)} ago"
22
73
  when 525960..1051920 then "about a year ago"
23
- else "over #{years_ago.round} years ago"
74
+ else "over #{years_ago.to_i} years ago"
24
75
  end
25
76
  end
26
77
 
27
78
  def seconds_ago
28
- Time.now.to_i - to_i
79
+ self
29
80
  end
30
81
 
31
82
  def minutes_ago
@@ -1,9 +1,9 @@
1
1
  require 'date'
2
2
  require 'downterm'
3
- require 'highline'
4
3
  require 'rainbow/ext/string'
5
4
  require 'redcarpet'
6
5
  require 'stringio'
6
+ require 'tty-screen'
7
7
  require 'usaidwat/ext/string'
8
8
  require 'usaidwat/ext/time'
9
9
 
@@ -12,25 +12,36 @@ Rainbow.enabled = true unless ENV['USAIDWAT_ENV'] == 'cucumber'
12
12
  module USaidWat
13
13
  module CLI
14
14
  class BaseFormatter
15
- attr_reader :pattern
16
-
17
- def initialize(pattern = nil, raw = false)
18
- @pattern = pattern
19
- @raw = raw
15
+ def initialize(options = {})
16
+ @options = options
20
17
  @count = 0
21
18
  end
22
19
 
20
+ def pattern
21
+ @options[:pattern]
22
+ end
23
+
23
24
  def pattern?
24
- !@pattern.nil?
25
+ !!@options[:pattern]
25
26
  end
26
27
 
27
28
  def raw?
28
- @raw
29
+ !!@options[:raw]
29
30
  end
31
+
32
+ def relative_dates?
33
+ @options[:date_format].nil? || @options[:date_format].to_sym != :absolute
34
+ end
35
+
36
+ protected
37
+
38
+ def tty
39
+ @tty || TTY::Screen.new
40
+ end
30
41
  end
31
42
 
32
43
  class CommentFormatter < BaseFormatter
33
- def initialize(pattern = nil, raw = false)
44
+ def initialize(options = {})
34
45
  @markdown = Redcarpet::Markdown.new(Downterm::Render::Terminal, :autolink => true,
35
46
  :strikethrough => true,
36
47
  :superscript => true)
@@ -38,13 +49,15 @@ module USaidWat
38
49
  end
39
50
 
40
51
  def format(comment)
41
- cols = HighLine::SystemExtensions.terminal_size[0]
52
+ cols = tty.width
42
53
  out = StringIO.new
43
54
  out.write("\n\n") unless @count == 0
44
55
  out.write("#{comment.subreddit}\n".color(:green))
45
56
  out.write("#{comment_link(comment)}\n".color(:yellow))
46
57
  out.write("#{comment.link_title.strip.truncate(cols)}\n".color(:magenta))
47
- out.write("#{comment_date(comment)}\n".color(:blue))
58
+ out.write("#{comment_date(comment)}".color(:blue))
59
+ out.write(" \u2022 ".color(:cyan))
60
+ out.write(sprintf("%+d\n", comment_karma(comment)).color(:blue))
48
61
  out.write("\n")
49
62
  out.write("#{comment_body(comment)}\n")
50
63
  @count += 1
@@ -54,7 +67,7 @@ module USaidWat
54
67
 
55
68
  private
56
69
  def comment_body(comment)
57
- body = comment.body
70
+ body = comment.body.strip
58
71
  body = @markdown.render(body) unless raw?
59
72
  if pattern?
60
73
  body.highlight(pattern)
@@ -69,13 +82,24 @@ module USaidWat
69
82
  end
70
83
 
71
84
  def comment_date(comment)
72
- DateTime.strptime(comment.created_utc.to_s, "%s").to_time.localtime.ago
85
+ d = DateTime.strptime(comment.created_utc.to_s, "%s").to_time.localtime
86
+ if relative_dates?
87
+ d.ago
88
+ else
89
+ d_part = d.strftime("%-d %b %Y")
90
+ t_part = d.strftime("%l:%M %p").strip
91
+ "#{d_part} #{t_part}"
92
+ end
93
+ end
94
+
95
+ def comment_karma(comment)
96
+ comment.ups - comment.downs
73
97
  end
74
98
  end
75
99
 
76
100
  class CompactCommentFormatter < BaseFormatter
77
101
  def format(comment)
78
- cols = HighLine::SystemExtensions.terminal_size[0]
102
+ cols = tty.width
79
103
  out = StringIO.new
80
104
  subreddit = comment.subreddit
81
105
  cols -= subreddit.length + 1
@@ -1,7 +1,7 @@
1
1
  module USaidWat
2
2
  module Service
3
3
  class MockComment
4
- attr_reader :subreddit, :body, :id, :link_id, :created_utc, :link_title
4
+ attr_reader :subreddit, :body, :id, :link_id, :created_utc, :link_title, :ups, :downs
5
5
 
6
6
  def initialize(dict)
7
7
  data = dict['data']
@@ -11,6 +11,8 @@ module USaidWat
11
11
  @link_id = data['link_id']
12
12
  @created_utc = data['created_utc']
13
13
  @link_title = data['link_title']
14
+ @ups = data['ups']
15
+ @downs = data['downs']
14
16
  end
15
17
  end
16
18
 
@@ -19,15 +21,21 @@ module USaidWat
19
21
  @username = username
20
22
  end
21
23
 
24
+ def about
25
+ load_data("user_#{@username}.json")['data']
26
+ end
27
+
22
28
  def comments(n)
23
- path = File.join(File.dirname(__FILE__), "..", "..", "features", "fixtures", "#{@username}.json")
24
- if File.exists?(path)
25
- json = IO.read(path)
26
- json = JSON.parse(json)
27
- json['data']['children'].map { |d| MockComment.new(d) }
28
- else
29
- raise USaidWat::Client::NoSuchUserError, @username
30
- end
29
+ json = load_data("#{@username}.json")
30
+ json['data']['children'].map { |d| MockComment.new(d) }
31
+ end
32
+
33
+ private
34
+
35
+ def load_data(data_file)
36
+ path = File.join(File.dirname(__FILE__), "..", "..", "features", "fixtures", data_file)
37
+ raise USaidWat::Client::NoSuchUserError, @username unless File.exists?(path)
38
+ JSON.parse(IO.read(path))
31
39
  end
32
40
  end
33
41
 
@@ -1,3 +1,3 @@
1
1
  module USaidWat
2
- VERSION = "1.1.1"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -4,19 +4,19 @@ module USaidWat
4
4
  module Client
5
5
  describe Client do
6
6
  let(:redditor) { Redditor.new("mipadi") }
7
-
7
+
8
8
  describe "#username" do
9
9
  it "returns the Redditor's username" do
10
10
  expect(redditor.username).to eq("mipadi")
11
11
  end
12
12
  end
13
-
13
+
14
14
  describe "#to_s" do
15
15
  it "returns a string representing the Redditor" do
16
16
  expect("#{redditor}").to eq("mipadi")
17
17
  end
18
18
  end
19
-
19
+
20
20
  context "when Reddit is up" do
21
21
  before(:each) do
22
22
  WebMock.disable_net_connect!
@@ -24,13 +24,47 @@ module USaidWat
24
24
  root = File.expand_path("../../../features/fixtures", __FILE__)
25
25
  stub_request(:get, "http://www.reddit.com/user/mipadi/comments.json?after=&limit=100").
26
26
  to_return(:body => IO.read(File.join(root, "mipadi.json")))
27
+ stub_request(:get, "http://www.reddit.com/user/mipadi/about.json").
28
+ to_return(:body => IO.read(File.join(root, "user_mipadi.json")))
29
+
30
+ Timecop.freeze(Time.new(2015, 9, 15, 11, 14, 30, "-07:00"))
27
31
  end
28
-
32
+
33
+ after(:each) do
34
+ Timecop.return
35
+ end
36
+
29
37
  describe "#comments" do
30
38
  it "retrieves 100 comments" do
31
39
  expect(redditor.comments.count).to eq(100)
32
40
  end
33
41
  end
42
+
43
+ describe "#link_karma" do
44
+ it "returns the user's link karma" do
45
+ expect(redditor.link_karma).to eq(4892)
46
+ end
47
+ end
48
+
49
+ describe "#comment_karma" do
50
+ it "returns the user's comment karma" do
51
+ expect(redditor.comment_karma).to eq(33440)
52
+ end
53
+ end
54
+
55
+ describe "#created_at" do
56
+ it "returns the date the account was created" do
57
+ expected = Time.new(2008, 3, 31, 15, 55, 26, "-07:00")
58
+ expect(redditor.created_at).to eq(expected)
59
+ end
60
+ end
61
+
62
+ describe "#age" do
63
+ it "returns a string describing the age of the account" do
64
+ expected = "over 7 years ago"
65
+ expect(redditor.age).to eq(expected)
66
+ end
67
+ end
34
68
  end
35
69
 
36
70
  context "when a Reddit user does not exist" do
@@ -40,6 +74,8 @@ module USaidWat
40
74
  root = File.expand_path("../../../features/fixtures", __FILE__)
41
75
  stub_request(:get, "http://www.reddit.com/user/testuser/comments.json?after=&limit=100").
42
76
  to_return(:status => 404, :body => IO.read(File.join(root, "testuser.json")))
77
+ stub_request(:get, "http://www.reddit.com/user/testuser/about.json").
78
+ to_return(:status => 404, :body => IO.read(File.join(root, "user_testuser.json")))
43
79
  end
44
80
 
45
81
  describe "#comments" do
@@ -47,21 +83,71 @@ module USaidWat
47
83
  expect { Redditor.new("testuser").comments }.to raise_error(NoSuchUserError, /testuser/)
48
84
  end
49
85
  end
86
+
87
+ describe "#link_karma" do
88
+ it "raises an exception if the user does not exist" do
89
+ expect { Redditor.new("testuser").link_karma }.to raise_error(NoSuchUserError, /testuser/)
90
+ end
91
+ end
92
+
93
+ describe "#comment_karma" do
94
+ it "raises an exception if the user does not exist" do
95
+ expect { Redditor.new("testuser").comment_karma }.to raise_error(NoSuchUserError, /testuser/)
96
+ end
97
+ end
98
+
99
+ describe "#created_at" do
100
+ it "raises an exception if the user does not exist" do
101
+ expect { Redditor.new("testuser").created_at }.to raise_error(NoSuchUserError, /testuser/)
102
+ end
103
+ end
104
+
105
+ describe "#age" do
106
+ it "raises an exception if the user does not exist" do
107
+ expect { Redditor.new("testuser").age }.to raise_error(NoSuchUserError, /testuser/)
108
+ end
109
+ end
50
110
  end
51
-
111
+
52
112
  context "when Reddit is down" do
53
113
  before(:each) do
54
114
  WebMock.disable_net_connect!
55
115
  WebMock.reset!
56
116
  stub_request(:get, "http://www.reddit.com/user/mipadi/comments.json?after=&limit=100").
57
117
  to_return(:status => 500)
118
+ stub_request(:get, "http://www.reddit.com/user/mipadi/about.json").
119
+ to_return(:status => 500)
58
120
  end
59
-
121
+
60
122
  describe "#comments" do
61
123
  it "raises 'Reddit unreachable' error" do
62
124
  expect { redditor.comments }.to raise_error(ReachabilityError, /Reddit unreachable/)
63
125
  end
64
126
  end
127
+
128
+ describe "#link_karma" do
129
+ it "raises 'Reddit unreachable' error" do
130
+ expect { redditor.link_karma }.to raise_error(ReachabilityError, /Reddit unreachable/)
131
+ end
132
+ end
133
+
134
+ describe "#comment_karma" do
135
+ it "raises 'Reddit unreachable' error" do
136
+ expect { redditor.comment_karma }.to raise_error(ReachabilityError, /Reddit unreachable/)
137
+ end
138
+ end
139
+
140
+ describe "#created_at" do
141
+ it "raises 'Reddit unreachable' error" do
142
+ expect { redditor.created_at }.to raise_error(ReachabilityError, /Reddit unreachable/)
143
+ end
144
+ end
145
+
146
+ describe "#age" do
147
+ it "raises 'Reddit unreachable' error" do
148
+ expect { redditor.age }.to raise_error(ReachabilityError, /Reddit unreachable/)
149
+ end
150
+ end
65
151
  end
66
152
  end
67
153
  end