dash-bees 0.18

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,49 @@
1
+ module DashFu::Bee
2
+ # Track Twitter links using the Backtype API.
3
+ class Backtweets
4
+ include DashFu::Bee
5
+
6
+ def setup(source, params)
7
+ url = params["url"].strip.downcase.sub(/^http(s?):\/\//, "")
8
+ source["source.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, callback)
19
+ session "backtweets.com" do |http|
20
+ http.get_json "/search.json?key=#{api_key}&q=#{Rack::Utils.escape source["url"]}" do |status, json|
21
+ if status == 200 && tweets = json["tweets"]
22
+ last_tweet_id = source["last_tweet_id"]
23
+ new_ones = tweets.take_while { |tweet| tweet["tweet_id"] != last_tweet_id }.reverse
24
+ new_ones.each do |tweet|
25
+ screen_name, id = tweet["tweet_from_user"], tweet["tweet_id"]
26
+ url = "http://twitter.com/#{screen_name}/#{id}"
27
+ html = <<-HTML
28
+ <a href="#{url}">tweeted</a>:
29
+ <blockquote>#{tweet["tweet_text"]}</blockquote>
30
+ HTML
31
+ person = { fullname: screen_name, identities: %W{twitter.com:#{screen_name}},
32
+ photo_url: "http://img.tweetimag.es/i/#{screen_name}_n" }
33
+ callback.activity! uid: id, url: url, html: html, tags: %w{twitter mention},
34
+ timestamp: Time.parse(tweet["tweet_created_at"]).utc, person: person
35
+ source["last_tweet_id"] = tweet["tweet_id"]
36
+ end
37
+ callback.set! tweets: json["totalresults"].to_i
38
+ else
39
+ callback.error! "Last request didn't go as expected, trying again later"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def meta(source)
46
+ [ { text: "Search yourself", url: "http://backtweets.com/search?q=#{URI.escape source["url"]}" } ]
47
+ end
48
+ end
49
+ 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,103 @@
1
+ module DashFu::Bee
2
+ # Track Github commits, watchers and forks.
3
+ class Github
4
+ include DashFu::Bee
5
+
6
+ def setup(source, params)
7
+ repo = params["repo"].strip
8
+ source["source.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, callback)
24
+ session "github.com", 443 do |http|
25
+ auth = { username: "#{source["username"]}/token", password: source["api_token"] }
26
+ http.get_json "/api/v2/json/repos/show/#{source["repo"]}", auth do |status, json|
27
+ case status
28
+ when 200
29
+ if repository = json["repository"]
30
+ source.update repository.slice(*%w{description url homepage})
31
+ callback.set! forks: repository["forks"], watchers: repository["watchers"]
32
+ end
33
+ when 404, 400
34
+ callback.error! "Could not find the repository #{source["repo"]}"
35
+ when 401
36
+ callback.error! "You are not authorized to access this repository, or invalid username/password"
37
+ else
38
+ callback.error! "Last request didn't go as expected, trying again later"
39
+ end
40
+ end
41
+
42
+ http.get_json "/api/v2/json/commits/list/#{source["repo"]}/#{source["branch"]}", auth do |status, json|
43
+ case status
44
+ when 200
45
+ if commits = json["commits"]
46
+ last_seen_id = source["last_commit"]["id"] if source["last_commit"]
47
+ new_ones = commits.take_while { |commit| commit["id"] != last_seen_id }
48
+ merged = new_ones.inject([]) do |all, commit|
49
+ last = all.last.last unless all.empty?
50
+ if last && last["committer"]["email"] == commit["committer"]["email"] &&
51
+ Time.parse(last["committed_date"]) - Time.parse(commit["committed_date"]) < 1.hour
52
+ all.last << commit
53
+ else
54
+ all << [commit]
55
+ end
56
+ all
57
+ end
58
+
59
+ merged.reverse.each do |commits|
60
+ first = commits.first
61
+ committer = first["committer"]
62
+ person = { fullname: committer["name"], identities: %W{github.com:#{committer["login"]}}, email: committer["email"] }
63
+ 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>} }
64
+ html = %{pushed to #{h source["branch"]} at <a href="http://github.com/#{source["repo"]}">#{h source["repo"]}</a>:\n#{messages.join("\n")}}
65
+ callback.activity! uid: first["id"], html: html, url: first["url"], tags: %w{push},
66
+ timestamp: Time.parse(first["committed_date"]).utc, person: person
67
+ end
68
+ callback.inc! commits: new_ones.count
69
+ if last_commit = commits.first
70
+ source.update "last_commit"=>last_commit.slice("id", "message", "url").
71
+ merge("timestamp"=>Time.parse(last_commit["committed_date"]).utc)
72
+ end
73
+ end
74
+ when 404, 400
75
+ callback.error! "Could not find the branch #{source["branch"]}"
76
+ else
77
+ callback.error! "Github: #{response.code} #{response.message}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def meta(source)
84
+ meta = [ { title: "Repository", text: source["repo"], url: source["url"] },
85
+ { title: "Branch", text: source["branch"] },
86
+ { text: source["description"] },
87
+ { title: "Home page", url: source["homepage"] } ]
88
+ if last_commit = source["last_commit"]
89
+ meta << { title: "Commit", text: last_commit["message"], url: last_commit["url"] }
90
+ end
91
+ meta
92
+ end
93
+
94
+ protected
95
+
96
+ def http_request(http, source, path)
97
+ get = Net::HTTP::Get.new(path.gsub(":repo", URI.escape(source["repo"])).gsub(":branch", URI.escape(source["branch"])))
98
+ get.basic_auth "#{source["username"]}/token", source["api_token"] unless source["username"].blank? && source["api_token"].blank?
99
+ http.request(get)
100
+ end
101
+
102
+ end
103
+ 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,90 @@
1
+ module DashFu::Bee
2
+ # Track open/closed Github issues.
3
+ class GithubIssues
4
+ include DashFu::Bee
5
+
6
+ def setup(source, params)
7
+ repo = params["repo"].to_s.strip
8
+ source["source.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, callback)
22
+ session "github.com", 443 do |http|
23
+ auth = { username: "#{source["username"]}/token", password: source["api_token"] }
24
+ http.get_json "/api/v2/json/issues/list/#{source["repo"]}/open" do |status, json|
25
+ case status
26
+ when 200
27
+ if open = json["issues"]
28
+ callback.set! open: open.count
29
+ open_ids = Set.new(source["open_ids"])
30
+ open.reject { |issue| open_ids.include?(issue["number"]) }.reverse.each do |issue|
31
+ sha = Digest::SHA1.hexdigest([source["repo"], "open", issue["number"], issue["updated_at"]].join(":"))
32
+ url = "http://github.com/#{source["repo"]}/issues#issue/#{issue["number"]}"
33
+ html = <<-HTML
34
+ opened <a href="#{url}">issue #{issue["number"]}</a> on #{source["repo"]}:
35
+ <blockquote>#{h issue["title"]}</blockquote>
36
+ HTML
37
+ callback.activity! uid: sha, url: url, html: html, tags: %w{issue opened},
38
+ timestamp: Time.parse(issue["created_at"]).utc
39
+ open_ids << issue["number"]
40
+ end
41
+ source["open_ids"] = open_ids.to_a
42
+ end
43
+ when 404, 400
44
+ callback.error! "Could not find the repository #{source["repo"]}"
45
+ when 401
46
+ callback.error! "You are not authorized to access this repository, or invalid username/password"
47
+ else
48
+ callback.error! "Last request didn't go as expected, trying again later"
49
+ end
50
+ end
51
+
52
+ http.get_json "/api/v2/json/issues/list/#{source["repo"]}/closed" do |status, json|
53
+ case status
54
+ when 200
55
+ if closed = json["issues"]
56
+ callback.set! closed: closed.count
57
+ closed_ids = Set.new(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
+ callback.activity! uid: sha, url: url, html: html, tags: %w{issue closed},
66
+ timestamp: Time.parse(issue["closed_at"]).utc
67
+ closed_ids << issue["number"]
68
+ end
69
+ source["closed_ids"] = closed_ids.to_a
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def meta(source)
77
+ [ { title: "On Github", url: "http://github.com/#{source["repo"]}/issues" } ]
78
+ end
79
+
80
+ protected
81
+
82
+ def http_request(http, source, state)
83
+ path = "/api/v2/json/issues/list/#{URI.escape source["repo"]}/#{state}"
84
+ get = Net::HTTP::Get.new(path)
85
+ get.basic_auth "#{source["username"]}/token", source["api_token"] unless source["username"].blank? && source["api_token"].blank?
86
+ http.request(get)
87
+ end
88
+
89
+ end
90
+ 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,54 @@
1
+ module DashFu::Bee
2
+ # Track activity (downloads and releases) of a Ruby Gem from rubygems.org.
3
+ class RubyGems
4
+ include DashFu::Bee
5
+
6
+ def setup(source, params)
7
+ gem_name = params["gem_name"].to_s.strip
8
+ source["source.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, callback)
19
+ gem_name = source["gem_name"]
20
+ session "rubygems.org" do |http|
21
+ http.get_json "/api/v1/gems/#{gem_name}.json" do |status, json|
22
+ case status
23
+ when 200
24
+ if source["version"]
25
+ current, latest = Gem::Version.new(source["version"]), Gem::Version.new(json["version"])
26
+ if latest > current
27
+ html = "released <a href=\"http://rubygems.org/gems/#{gem_name}/versions/#{latest}\">#{gem_name} version #{latest}</a>."
28
+ person = { fullname: "RubyGems", identities: "url:http://rubygems.org/", photo_url: "http://dash-fu.com/images/sources/ruby.png" }
29
+ callback.activity! uid: "#{gem_name}-#{latest}", url: "http://rubygems.org/gems/#{gem_name}",
30
+ html: html, tags: %w{release}, person: person
31
+ end
32
+ end
33
+ source["version"] = json["version"]
34
+ source.update json.slice(*%w{homepage_uri project_uri info authors info})
35
+ callback.set! downloads: json["downloads"]
36
+ when 404
37
+ callback.error! "Could not find the Gem #{gem_name}"
38
+ else
39
+ callback.error! "Last request didn't go as expected, trying again later"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def meta(source)
46
+ [ { title: "Project", text: source["gem_name"], url: source["homepage_uri"] || source["project_uri"] },
47
+ { text: source["info"] },
48
+ { title: "Version", text: source["version"] },
49
+ { title: "Authors", text: source["authors"] },
50
+ { title: "Source", text: "RubyGems", url: source["project_uri"] } ]
51
+ end
52
+
53
+ end
54
+ 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.
@@ -0,0 +1 @@
1
+ backtweets: "4554feeeeeeeeeeeeeee"
@@ -0,0 +1,148 @@
1
+ require_relative "setup"
2
+
3
+ test DashFu::Bee::Backtweets do
4
+ context "setup" do
5
+ setup { source.setup "url"=>"vanity.labnotes.org" }
6
+
7
+ should "use normalize URL and remove scheme" do
8
+ source.setup "url"=>"http://Vanity.Labnotes.Org"
9
+ assert_equal "Tweets for vanity.labnotes.org", source.name
10
+ end
11
+
12
+ context "metric" do
13
+ subject { source.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) { source.setup "url"=>" " }
33
+ end
34
+
35
+ should "create valid metric" do
36
+ source.setup "url"=>"vanity.labnotes.org"
37
+ assert source.valid?
38
+ end
39
+ end
40
+
41
+
42
+ context "update" do
43
+ setup { source.setup "url"=>"vanity.labnotes.org" }
44
+
45
+ should "include API key" do
46
+ source.update
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
+ stub_request(:get, /backtweets.com/).to_return body: interactions.first.response.body
52
+ source.setup "url"=>"need&this=encoded"
53
+ source.update
54
+ assert_requested :get, "http://backtweets.com/search.json?key=#{bee.send :api_key}&q=need%26this%3Dencoded"
55
+ end
56
+
57
+ should "handle errors" do
58
+ stub_request(:get, interactions.first.uri).to_return status: 500
59
+ source.update
60
+ assert_equal "Last request didn't go as expected, trying again later", source.last_error
61
+ end
62
+
63
+ should "handle invalid document entity" do
64
+ stub_request(:get, interactions.first.uri).to_return body: "Not JSON"
65
+ source.update
66
+ assert_equal "Last request didn't go as expected, trying again later", source.last_error
67
+ end
68
+
69
+ should "capture number of tweets" do
70
+ source.update
71
+ assert_equal({ tweets: 2 }, source.metric.values)
72
+ end
73
+
74
+ context "activity" do
75
+ setup { source.update }
76
+ subject { source.activity }
77
+
78
+ should "capture tweet identifier" do
79
+ assert_equal "20959239143", subject.uid
80
+ end
81
+
82
+ should "capture tweet text" do
83
+ assert_equal <<-HTML, subject.html
84
+ <a href="http://twitter.com/dude/20959239143">tweeted</a>:
85
+ <blockquote>Super awesome <a href="http://vanity.labnotes.org/">http://j.mp/aOrUnsz</a></blockquote>
86
+ HTML
87
+ end
88
+
89
+ should "capture tweet URL" do
90
+ assert_equal "http://twitter.com/dude/20959239143", subject.url
91
+ end
92
+
93
+ should "capture tweet timestamp" do
94
+ assert_equal Time.parse("2010-8-12T08:30:04").utc, subject.timestamp
95
+ end
96
+
97
+ should "tag as tweeter and mention" do
98
+ assert_contains subject.tags, "twitter"
99
+ assert_contains subject.tags, "mention"
100
+ end
101
+
102
+ should "be valid" do
103
+ assert subject.valid?
104
+ end
105
+
106
+ context "author" do
107
+ subject { source.activity.person }
108
+
109
+ should "capture full name" do
110
+ assert_equal "dude", subject.fullname
111
+ end
112
+
113
+ should "capture screen name" do
114
+ assert_contains subject.identities, "twitter.com:dude"
115
+ end
116
+
117
+ should "capture photo" do
118
+ assert_equal "http://img.tweetimag.es/i/dude_n", subject.photo_url
119
+ end
120
+ end
121
+ end
122
+
123
+ context "repeating" do
124
+ setup do
125
+ source.update
126
+ second = interactions.last
127
+ stub_request(:get, second.uri).to_return body: second.response.body
128
+ source.update
129
+ end
130
+
131
+ should "capture new tweets" do
132
+ assert_equal 3, source.activities.count
133
+ assert_match "The last of its kind", source.activity.html
134
+ end
135
+
136
+ end
137
+ end
138
+
139
+
140
+ context "meta" do
141
+ setup { source.setup "url"=>"vanity.labnotes.org" }
142
+ subject { source.meta }
143
+
144
+ should "link to search results" do
145
+ assert subject.include?(text: "Search yourself", url: "http://backtweets.com/search?q=vanity.labnotes.org")
146
+ end
147
+ end
148
+ end