dash-mario 0.15

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,57 @@
1
+ module DashFu::Mario
2
+ # Track Twitter links using the Backtype API.
3
+ class Backtweets
4
+ include DashFu::Mario
5
+
6
+ def setup(source, params)
7
+ url = params["url"].strip.downcase.sub(/^http(s?):\/\//, "")
8
+ source["metric.name"] = "Tweets for #{url}"
9
+ source["metric.columns"] = [{ :id=>"tweets", :label=>"Tweets" }]
10
+ source["metric.totals"] = true
11
+ source["url"] = url
12
+ end
13
+
14
+ def validate(source)
15
+ raise "Missing URL" if source["url"].blank?
16
+ end
17
+
18
+ def update(source, request, &block)
19
+ Net::HTTP.start "backtweets.com" do |http|
20
+ get = Net::HTTP::Get.new("/search.json?key=#{api_key}&q=#{Rack::Utils.escape source["url"]}")
21
+ response = http.request(get)
22
+ json = JSON.parse(response.body) rescue nil if Net::HTTPOK === response
23
+ unless json
24
+ logger.error "Backtweets: #{response.code} #{response.message}"
25
+ raise "Last request didn't go as expected, trying again later"
26
+ end
27
+
28
+ if tweets = json["tweets"]
29
+ last_tweet_id = source["last_tweet_id"]
30
+ if last_tweet_id
31
+ new_ones = tweets.take_while { |tweet| tweet["tweet_id"] != last_tweet_id }.reverse
32
+ new_ones.each do |tweet|
33
+ handle, id = tweet["tweet_from_user"], tweet["tweet_id"]
34
+ url = "http://twitter.com/#{handle}/#{id}"
35
+ html = <<-HTML
36
+ <a href="#{url}">tweeted</a>:
37
+ <blockquote>#{tweet["tweet_text"]}</blockquote>
38
+ HTML
39
+ person = { :fullname=>handle, :identities=>%W{twitter.com:#{handle}}, :photo_url=>tweet["tweet_profile_image_url"] }
40
+ block.call :activity=>{ :uid=>id, :url=>url, :html=>html, :tags=>%w{twitter mention},
41
+ :timestamp=>Time.parse(tweet["tweet_created_at"]).utc, :person=>person }
42
+ end
43
+ end
44
+ if recent = tweets.first
45
+ source["last_tweet_id"] = recent["tweet_id"]
46
+ end
47
+ end
48
+
49
+ block.call :set=>{ :tweets=>json["totalresults"].to_i }
50
+ end
51
+ end
52
+
53
+ def meta(source)
54
+ [ { :text=>"Search yourself", :url=>"http://backtweets.com/search?q=#{URI.escape source["url"]}" } ]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ en:
2
+ description: "Finds Tweets linking to any URL"
3
+ inputs: |-
4
+ <label>URL <input type="text" name="source[url]" size="30"></label>
5
+ notes: |-
6
+ <ul>
7
+ <li>If this is a new search, it may take some time before we get the first results (including historical data). Check back in 8 hours.</li>
8
+ <li>Data provided by <a href="http://backtweets.com">Backtweets</a></li>
9
+ </ul>
@@ -0,0 +1,110 @@
1
+ module DashFu::Mario
2
+ # Track Github commits, watchers and forks.
3
+ class Github
4
+ include DashFu::Mario
5
+
6
+ def setup(source, params)
7
+ repo = params["repo"].strip
8
+ source["metric.name"] = "Github: #{repo}"
9
+ source["metric.columns"] = [{ :id=>"commits", :label=>"Commits" }, { :id=>"watchers", :label=>"Watchers" }, { :id=>"forks", :label=>"Forks" }]
10
+ source["metric.totals"] = true
11
+ source["repo"] = repo
12
+ branch = params["branch"].strip
13
+ source["branch"] = branch.blank? ? "master" : branch
14
+ source["username"] = params["username"]
15
+ source["api_token"] = params["api_token"]
16
+ end
17
+
18
+ def validate(source)
19
+ raise "Missing repository name" if source["repo"].blank?
20
+ raise "Need user name and repository name, e.g. assaf/vanity" unless source["repo"][/^[\w-]+\/[\w-]+$/]
21
+ end
22
+
23
+ def update(source, request, &block)
24
+ http = Net::HTTP.new("github.com", 443)
25
+ http.use_ssl = true
26
+ http.start do
27
+ case response = http_request(http, source, "/api/v2/json/repos/show/:repo")
28
+ when Net::HTTPOK
29
+ json = JSON.parse(response.body)["repository"] rescue nil
30
+ when Net::HTTPNotFound, Net::HTTPBadRequest
31
+ raise "Could not find the repository #{source["repo"]}"
32
+ when Net::HTTPUnauthorized
33
+ raise "You are not authorized to access this repository, or invalid username/password"
34
+ end
35
+ if json
36
+ source.update json.slice(*%w{description url homepage})
37
+ block.call :set=>{ :forks=>json["forks"], :watchers=>json["watchers"] }
38
+ else
39
+ logger.error "Github: #{response.code} #{response.message}"
40
+ error = true
41
+ end
42
+
43
+ case response = http_request(http, source, "/api/v2/json/commits/list/:repo/:branch")
44
+ when Net::HTTPOK
45
+ commits = JSON.parse(response.body)["commits"] rescue nil
46
+ when Net::HTTPNotFound, Net::HTTPBadRequest
47
+ raise "Could not find the branch #{source["branch"]}"
48
+ end
49
+
50
+ if commits
51
+ last_seen_id = source["last_commit"]["id"] if source["last_commit"]
52
+ if last_seen_id
53
+ new_ones = commits.take_while { |commit| commit["id"] != last_seen_id }
54
+ merged = new_ones.inject([]) do |all, commit|
55
+ last = all.last.last unless all.empty?
56
+ if last && last["committer"]["email"] == commit["committer"]["email"]
57
+ all.last << commit
58
+ else
59
+ all << [commit]
60
+ end
61
+ all
62
+ end
63
+
64
+ merged.reverse.each do |commits|
65
+ first = commits.first
66
+ committer = first["committer"]
67
+ person = { :fullname=>committer["name"], :identities=>%W{github.com:#{committer["login"]}}, :email=>committer["email"] }
68
+ messages = commits.map { |commit| %{<blockquote><a href="#{commit["url"]}">#{commit["id"][0,7]}</a> #{h commit["message"].strip.split(/[\n\r]/).first[0,50]}</blockquote>} }
69
+ html = %{pushed to #{h source["branch"]} at <a href="http://github.com/#{source["repo"]}">#{h source["repo"]}</a>:\n#{messages.join("\n")}}
70
+ block.call :activity=>{ :uid=>first["id"], :html=>html, :url=>first["url"], :tags=>%w{push},
71
+ :timestamp=>Time.parse(first["committed_date"]).utc, :person=>person }
72
+ end
73
+ block.call :inc=>{ :commits=>new_ones.count }
74
+ else
75
+ block.call :set=>{ :commits=>commits.count }
76
+ end
77
+ if last_commit = commits.first
78
+ source.update "last_commit"=>last_commit.slice("id", "message", "url").
79
+ merge("timestamp"=>Time.parse(last_commit["committed_date"]).utc)
80
+ end
81
+ else
82
+ logger.error "Github: #{response.code} #{response.message}"
83
+ error = true
84
+ end
85
+
86
+ raise "Last request didn't go as expected, trying again later" if error
87
+ end
88
+ end
89
+
90
+ def meta(source)
91
+ meta = [ { :title=>"Repository", :text=>source["repo"], :url=>source["url"] },
92
+ { :title=>"Branch", :text=>source["branch"] },
93
+ { :text=>source["description"] },
94
+ { :title=>"Home page", :url=>source["homepage"] } ]
95
+ if last_commit = source["last_commit"]
96
+ meta << { :title=>"Commit", :text=>last_commit["message"], :url=>last_commit["url"] }
97
+ end
98
+ meta
99
+ end
100
+
101
+ protected
102
+
103
+ def http_request(http, source, path)
104
+ get = Net::HTTP::Get.new(path.gsub(":repo", URI.escape(source["repo"])).gsub(":branch", URI.escape(source["branch"])))
105
+ get.basic_auth "#{source["username"]}/token", source["api_token"] unless source["username"].blank? && source["api_token"].blank?
106
+ http.request(get)
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,15 @@
1
+ en:
2
+ description: "Tracks Github commits, watchers and forks"
3
+ inputs: |-
4
+ <label>User/repository <input type="text" name="source[repo]" size="30"></label>
5
+ <label>Branch <input type="text" name="source[branch]" size="30" value="master"></label>
6
+ <p>For private repositories:</p>
7
+ <label>Username <input type="text" name="source[username]" size="30"></label>
8
+ <label><a href="https://github.com/account#admin_bucket" target="github">API Token</a> <input type="password" name="source[api_token]" size="30"></label>
9
+ notes: |-
10
+ <ul>
11
+ <li>Enter user name/repository like this: <code>assaf/vanity</code></li>
12
+ <li>To access private repositories, authenticate using your username and API token.
13
+ You can find your API token in <a href="https://github.com/account#admin_bucket" target="github">Account Settings</a>.
14
+ </li>
15
+ </ul>
@@ -0,0 +1,91 @@
1
+ module DashFu::Mario
2
+ # Track open/closed Github issues.
3
+ class GithubIssues
4
+ include DashFu::Mario
5
+
6
+ def setup(source, params)
7
+ repo = params["repo"].to_s.strip
8
+ source["metric.name"] = "Github Issues for #{repo}"
9
+ source["metric.columns"] = [{ :id=>"open", :label=>"Open issues" }, { :id=>"closed", :label=>"Closed issues" }]
10
+ source["metric.totals"] = true
11
+ source["repo"] = repo
12
+ source["username"] = params["username"]
13
+ source["api_token"] = params["api_token"]
14
+ end
15
+
16
+ def validate(source)
17
+ raise "Missing user/repository" if source["repo"].blank?
18
+ raise "Need user name and repository name, e.g. assaf/vanity" unless source["repo"][/^[\w-]+\/[\w-]+$/]
19
+ end
20
+
21
+ def update(source, request, &block)
22
+ http = Net::HTTP.new("github.com", 443)
23
+ http.use_ssl = true
24
+ http.start do
25
+ update = {}
26
+ case response = http_request(http, source, "open")
27
+ when Net::HTTPOK
28
+ open = JSON.parse(response.body)["issues"] rescue nil
29
+ when Net::HTTPNotFound, Net::HTTPBadRequest
30
+ raise "Could not find the repository #{source["repo"]}"
31
+ when Net::HTTPUnauthorized
32
+ raise "You are not authorized to access this repository, or invalid username/password"
33
+ end
34
+ if open
35
+ update[:open] = open.count
36
+ if open_ids = source["open-ids"]
37
+ open.reject { |issue| open_ids.include?(issue["number"]) }.reverse.each do |issue|
38
+ sha = Digest::SHA1.hexdigest([source["repo"], "open", issue["number"], issue["updated_at"]].join(":"))
39
+ url = "http://github.com/#{source["repo"]}/issues#issue/#{issue["number"]}"
40
+ html = <<-HTML
41
+ opened <a href="#{url}">issue #{issue["number"]}</a> on #{source["repo"]}:
42
+ <blockquote>#{h issue["title"]}</blockquote>
43
+ HTML
44
+ block.call :activity=>{ :uid=>sha, :url=>url, :html=>html, :tags=>%w{issue opened},
45
+ :timestamp=>Time.parse(issue["created_at"]).utc }
46
+ end
47
+ end
48
+ source["open-ids"] = open.map { |issue| issue["number"] }
49
+ end
50
+
51
+ case response = http_request(http, source, "closed")
52
+ when Net::HTTPOK
53
+ closed = JSON.parse(response.body)["issues"] rescue nil
54
+ end
55
+ if closed
56
+ update[:closed] = closed.count
57
+ if closed_ids = source["closed-ids"]
58
+ closed.reject { |issue| closed_ids.include?(issue["number"]) }.reverse.each do |issue|
59
+ sha = Digest::SHA1.hexdigest([source["repo"], "closed", issue["number"], issue["updated_at"]].join(":"))
60
+ url = "http://github.com/#{source["repo"]}/issues#issue/#{issue["number"]}"
61
+ html = <<-HTML
62
+ closed <a href="#{url}">issue #{issue["number"]}</a> on #{source["repo"]}:
63
+ <blockquote>#{h issue["title"]}</blockquote>
64
+ HTML
65
+ block.call :activity=>{ :uid=>sha, :url=>url, :html=>html, :tags=>%w{issue closed},
66
+ :timestamp=>Time.parse(issue["closed_at"]).utc }
67
+ end
68
+ end
69
+ source["closed-ids"] = closed.map { |issue| issue["number"] }
70
+ end
71
+
72
+ raise "Last request didn't go as expected, trying again later" if update.empty?
73
+ block.call :set=>update
74
+ end
75
+ end
76
+
77
+ def meta(source)
78
+ [ { :title=>"On Github", :url=>"http://github.com/#{source["repo"]}/issues" } ]
79
+ end
80
+
81
+ protected
82
+
83
+ def http_request(http, source, state)
84
+ path = "/api/v2/json/issues/list/#{URI.escape source["repo"]}/#{state}"
85
+ get = Net::HTTP::Get.new(path)
86
+ get.basic_auth "#{source["username"]}/token", source["api_token"] unless source["username"].blank? && source["api_token"].blank?
87
+ http.request(get)
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,14 @@
1
+ en:
2
+ description: "Tracks open/closed Github issues"
3
+ inputs: |-
4
+ <label>User/repository <input type="text" name="source[repo]" size="30"></label>
5
+ <p>For private repositories:</p>
6
+ <label>Username <input type="text" name="source[username]" size="30"></label>
7
+ <label><a href="https://github.com/account#admin_bucket" target="github">API Token</a> <input type="password" name="source[api_token]" size="30"></label>
8
+ notes: |-
9
+ <ul>
10
+ <li>Enter user name/repository like this: <code>assaf/vanity</code></li>
11
+ <li>To access private repositories, authenticate using your username and API token.
12
+ You can find your API token in <a href="https://github.com/account#admin_bucket" target="github">Account Settings</a>.
13
+ </li>
14
+ </ul>
@@ -0,0 +1,55 @@
1
+ module DashFu::Mario
2
+ # Track activity (downloads and releases) of a Ruby Gem from rubygems.org.
3
+ class RubyGems
4
+ include DashFu::Mario
5
+
6
+ def setup(source, params)
7
+ gem_name = params["gem_name"].to_s.strip
8
+ source["metric.name"] = "RubyGems: #{gem_name}"
9
+ source["metric.columns"] = [{ :id=>"downloads", :label=>"Downloads" }]
10
+ source["metric.totals"] = true
11
+ source["gem_name"] = gem_name
12
+ end
13
+
14
+ def validate(source)
15
+ raise "Missing gem name" if source["gem_name"].blank?
16
+ end
17
+
18
+ def update(source, request)
19
+ gem_name = source["gem_name"]
20
+ uri = URI.parse("http://rubygems.org/api/v1/gems/#{Rack::Utils.escape gem_name}.json")
21
+ case response = Net::HTTP.get_response(uri)
22
+ when Net::HTTPOK
23
+ json = JSON.parse(response.body) rescue nil
24
+ when Net::HTTPNotFound
25
+ raise "Could not find the Gem #{gem_name}"
26
+ end
27
+ unless json
28
+ logger.error "RubyGems: #{response.code} #{response.message}"
29
+ raise "Last request didn't go as expected, trying again later"
30
+ end
31
+
32
+ if source["version"]
33
+ current, latest = Gem::Version.new(source["version"]), Gem::Version.new(json["version"])
34
+ if latest > current
35
+ html = "released <a href=\"http://rubygems.org/gems/#{gem_name}/versions/#{latest}\">#{gem_name} version #{latest}</a>."
36
+ person = { :fullname=>"RubyGems", :identities=>"url:http://rubygems.org/", :photo_url=>"http://dash-fu.com/images/sources/ruby.png" }
37
+ yield :activity=>{ :uid=>"#{gem_name}-#{latest}", :url=>"http://rubygems.org/gems/#{gem_name}",
38
+ :html=>html, :tags=>%w{release}, :person=>person }
39
+ end
40
+ end
41
+ source["version"] = json["version"]
42
+ source.update json.slice(*%w{homepage_uri project_uri info authors info})
43
+ yield :set=>{ :downloads=>json["downloads"] }
44
+ end
45
+
46
+ def meta(source)
47
+ [ { :title=>"Project", :text=>source["gem_name"], :url=>source["homepage_uri"] || source["project_uri"] },
48
+ { :text=>source["info"] },
49
+ { :title=>"Version", :text=>source["version"] },
50
+ { :title=>"Authors", :text=>source["authors"] },
51
+ { :title=>"Source", :text=>"RubyGems", :url=>source["project_uri"] } ]
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,6 @@
1
+ en:
2
+ description: "Tracks downloads and releases of a Ruby Gem on rubygems.org"
3
+ inputs: |-
4
+ <label>Gem name <input type="text" name="source[gem_name]" size="30"></label>
5
+ notes: |-
6
+ Gem names are case sensitive.
data/test/api_keys.yml ADDED
@@ -0,0 +1 @@
1
+ backtweets: "4554feeeeeeeeeeeeeee"
@@ -0,0 +1,151 @@
1
+ require_relative "setup"
2
+
3
+ test DashFu::Mario::Backtweets do
4
+ context "setup" do
5
+ setup { setup_source "url"=>"vanity.labnotes.org" }
6
+
7
+ should "use normalize URL and remove scheme" do
8
+ setup_source "url"=>"http://Vanity.Labnotes.Org"
9
+ assert_equal "Tweets for vanity.labnotes.org", metric.name
10
+ end
11
+
12
+ context "metric" do
13
+ subject { metric }
14
+
15
+ should "use URL" do
16
+ assert_equal "Tweets for vanity.labnotes.org", subject.name
17
+ end
18
+
19
+ should "measure totals" do
20
+ assert subject.totals
21
+ end
22
+
23
+ should "capture tweets" do
24
+ assert subject.columns.include?(:id=>"tweets", :label=>"Tweets")
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+ context "validation" do
31
+ should "raise error if URL missing" do
32
+ assert_raise(RuntimeError) { setup_source "url"=>" " }
33
+ end
34
+
35
+ should "create valid metric" do
36
+ setup_source "url"=>"vanity.labnotes.org"
37
+ assert metric.valid?
38
+ end
39
+ end
40
+
41
+
42
+ context "update" do
43
+ setup { setup_source "url"=>"vanity.labnotes.org" }
44
+
45
+ should "include API key" do
46
+ update_source
47
+ assert_requested :get, "http://backtweets.com/search.json?key=4554feeeeeeeeeeeeeee&q=vanity.labnotes.org"
48
+ end
49
+
50
+ should "properly encode URL" do
51
+ setup_source "url"=>"need&this=encoded"
52
+ update_source rescue nil
53
+ assert_requested :get, "http://backtweets.com/search.json?key=#{mario.send :api_key}&q=need%26this%3Dencoded"
54
+ end
55
+
56
+ should "handle errors" do
57
+ stub_request(:get, interactions.first.uri).to_return :status=>500
58
+ assert_raise(RuntimeError) { update_source }
59
+ assert_equal "Last request didn't go as expected, trying again later", last_error
60
+ end
61
+
62
+ should "handle invlid document entity" do
63
+ stub_request(:get, interactions.first.uri).to_return :body=>"Not JSON"
64
+ assert_raise(RuntimeError) { update_source }
65
+ assert_equal "Last request didn't go as expected, trying again later", last_error
66
+ end
67
+
68
+ should "capture number of tweets" do
69
+ update_source
70
+ assert_equal({ :tweets=>11 }, totals)
71
+ end
72
+
73
+ should "ignore previous tweets" do
74
+ update_source
75
+ assert activities.empty?
76
+ end
77
+
78
+ context "repeating" do
79
+ setup do
80
+ update_source
81
+ second = interactions.last
82
+ stub_request(:get, second.uri).to_return :body=>second.response.body
83
+ update_source
84
+ end
85
+
86
+ should "capture new tweets" do
87
+ assert_equal 2, activities.count
88
+ end
89
+
90
+ context "activity" do
91
+ subject { activity }
92
+
93
+ should "capture tweet identifier" do
94
+ assert_equal "20959239300", subject.uid
95
+ end
96
+
97
+ should "capture tweet text" do
98
+ assert_equal <<-HTML, subject.html
99
+ <a href="http://twitter.com/assaf/20959239300">tweeted</a>:
100
+ <blockquote>Super awesome <a href="http://vanity.labnotes.org/">http://j.mp/aOrUnsz</a></blockquote>
101
+ HTML
102
+ end
103
+
104
+ should "capture tweet URL" do
105
+ assert_equal "http://twitter.com/assaf/20959239300", subject.url
106
+ end
107
+
108
+ should "capture tweet timestamp" do
109
+ assert_equal Time.parse("2010-8-22T05:00:04").utc, subject.timestamp
110
+ end
111
+
112
+ should "tag as tweeter and mention" do
113
+ assert_contains subject.tags, "twitter"
114
+ assert_contains subject.tags, "mention"
115
+ end
116
+
117
+ should "be valid" do
118
+ assert subject.valid?
119
+ end
120
+
121
+ context "author" do
122
+ subject { activity.person }
123
+
124
+ should "capture full name" do
125
+ assert_equal "assaf", subject.fullname
126
+ end
127
+
128
+ should "capture screen name" do
129
+ assert_contains subject.identities, "twitter.com:assaf"
130
+ end
131
+
132
+ should "capture photo" do
133
+ assert_equal "http://twitter.com/account/profile_image/assaf", subject.photo_url
134
+ end
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+
142
+
143
+ context "meta" do
144
+ setup { setup_source "url"=>"vanity.labnotes.org" }
145
+ subject { meta }
146
+
147
+ should "link to search results" do
148
+ assert subject.include?(:text=>"Search yourself", :url=>"http://backtweets.com/search?q=vanity.labnotes.org")
149
+ end
150
+ end
151
+ end