code_review_notifier 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: edabbf2ed573f0042d93ae9559b8345bfa2b4ad697db78b43a1d6e736d06355e
4
+ data.tar.gz: 5b9be1ccc0d16556a7045e4b639ae809c9afea160461581b9bf6dd95cb137bae
5
+ SHA512:
6
+ metadata.gz: 567ac263894da8edae616278d2d558b9efce9f29987f24feb5497447b4d49f8328aa35beefff1c51c244ea1cc01b33c5445c441a485f50d056ed2b12c82e3926
7
+ data.tar.gz: 9f89909ef2d81828fb9d06b4e0d9a5bcb418b45c3814a584bd0845cafba0f4fc26d4c6a064d6deb7eab8e96aa99e21086a465f9892b90d10f4013e1a6d231606
@@ -0,0 +1,2 @@
1
+ brew 'sqlite'
2
+ brew 'terminal-notifier'
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem "sqlite3"
6
+ gem "httparty"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'code_review_notifier'
4
+
5
+ CodeReviewNotifier.call(ARGV)
@@ -0,0 +1,7 @@
1
+ require_relative "./gerrit_api.rb"
2
+
3
+ class Api
4
+ def self.current_api
5
+ GerritApi
6
+ end
7
+ end
@@ -0,0 +1,62 @@
1
+ require "httparty"
2
+ require_relative "./gerrit_api.rb"
3
+
4
+ class HTTParty::Parser
5
+ def json
6
+ JSON.parse(body.gsub(")]}'", ""))
7
+ end
8
+ end
9
+
10
+ class BaseApi
11
+ include HTTParty
12
+
13
+ def self.authenticate
14
+ raise NotImplementedError
15
+ end
16
+
17
+ def self.all_code_changes
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def self.favicon
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def self.code_change_url(code_change)
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def self.wrap_with_authentication(&block)
30
+ res = block.call
31
+ if res.code == 401 || res.code == 403
32
+ self.authenticate
33
+ block.call
34
+ else
35
+ res
36
+ end
37
+ end
38
+
39
+ def self.is_setup?
40
+ base_api_url && username && password && account_id
41
+ end
42
+
43
+ def self.base_api_url
44
+ @base_api_url ||= begin
45
+ url = DB.get_setting("base_api_url")
46
+ base_uri(url)
47
+ url
48
+ end
49
+ end
50
+
51
+ def self.username
52
+ @username ||= DB.get_setting("username")
53
+ end
54
+
55
+ def self.password
56
+ @password ||= DB.get_setting("password")
57
+ end
58
+
59
+ def self.account_id
60
+ @account_id ||= DB.get_setting("account_id")
61
+ end
62
+ end
@@ -0,0 +1,31 @@
1
+ require "base64"
2
+ require "openssl"
3
+ require "digest/sha1"
4
+
5
+ KEY = ""
6
+
7
+ class Cipher
8
+ def self.encrypt(text)
9
+ cipher = OpenSSL::Cipher.new("aes-256-cbc")
10
+ cipher.encrypt
11
+ key = Digest::SHA1.hexdigest("yourpass")[0..31]
12
+ iv = cipher.random_iv
13
+ cipher.key = key
14
+ cipher.iv = iv
15
+ encrypted = cipher.update(text)
16
+ encrypted << cipher.final
17
+ ["#{key}:#{Base64.encode64(iv).encode('utf-8')}", Base64.encode64(encrypted).encode('utf-8')]
18
+ end
19
+
20
+ def self.decrypt(encrypted, salt)
21
+ key, iv = salt.split(":")
22
+ iv = Base64.decode64(iv.encode('ascii-8bit'))
23
+ cipher = OpenSSL::Cipher.new("aes-256-cbc")
24
+ cipher.decrypt
25
+ cipher.key = key
26
+ cipher.iv = iv
27
+ decrypted = cipher.update(Base64.decode64(encrypted.encode('ascii-8bit')))
28
+ decrypted << cipher.final
29
+ decrypted
30
+ end
31
+ end
@@ -0,0 +1,62 @@
1
+ require "io/console"
2
+ require_relative "./api.rb"
3
+ require_relative "./notifier.rb"
4
+
5
+ SECONDS_BETWEEN_RUNS = 120
6
+ SECONDS_BETWEEN_NOTIFICATIONS = 5
7
+
8
+ class CodeReviewNotifier
9
+ def self.setup(args)
10
+ system("mkdir -p ~/.code_review_notifier")
11
+ system("brew bundle --file #{File.expand_path(File.dirname(__FILE__) + "/..")}/Brewfile")
12
+
13
+ if args[0] == "--setup" || !Api.current_api.is_setup?
14
+ print("What's the base URL? (i.e. https://gerrit.google.com) ")
15
+ DB.save_setting("base_api_url", STDIN.gets.chomp, is_secret: false)
16
+
17
+ print("What's the account username? ")
18
+ DB.save_setting("username", STDIN.gets.chomp, is_secret: false)
19
+
20
+ print("What's the account password? (hiding input) ")
21
+ DB.save_setting("password", STDIN.noecho(&:gets).chomp, is_secret: true)
22
+ puts
23
+
24
+ print("What's the account id? (check #{Api.current_api.base_api_url}/settings/) ")
25
+ DB.save_setting("account_id", STDIN.gets.chomp, is_secret: false)
26
+
27
+ puts("All setup!")
28
+ end
29
+ end
30
+
31
+ def self.call(args)
32
+ setup(args)
33
+
34
+ while true
35
+ is_first_run = is_first_run?
36
+ puts
37
+ puts("Querying API...")
38
+ all_code_changes = Api.current_api.all_code_changes
39
+ puts("Checking for notifications to display...")
40
+ all_activity = []
41
+ all_code_changes.each do |cc|
42
+ activity_for_code_change = cc.code_change_activity
43
+ activity_for_code_change.sort! { |a, b| a.created_at <=> b.created_at }
44
+ cc.activity_from_self_at = activity_for_code_change.find { |a| a.is_self }&.created_at
45
+ all_activity.concat(activity_for_code_change)
46
+ end
47
+ all_activity.select(&:should_notify?).each do |code_change_activity|
48
+ code_change_activity.notified
49
+ unless is_first_run
50
+ puts("Notifying of change!")
51
+ Notifier.notify(code_change_activity)
52
+ sleep SECONDS_BETWEEN_NOTIFICATIONS
53
+ end
54
+ end
55
+ sleep SECONDS_BETWEEN_RUNS
56
+ end
57
+ end
58
+
59
+ def self.is_first_run?
60
+ DB.query_single_row("SELECT id FROM code_change_activity_notified;").nil?
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+ require "sqlite3"
2
+ require_relative "./cipher.rb"
3
+
4
+ class DB
5
+ def self.db
6
+ @db ||= begin
7
+ db = SQLite3::Database.new(File.expand_path("~/.code_review_notifier/data.db"))
8
+ migrate_if_needed(db)
9
+ db
10
+ end
11
+ end
12
+
13
+ def self.migrate_if_needed(db)
14
+ db.execute("CREATE TABLE IF NOT EXISTS code_change_activity_notified (id VARCHAR(50) PRIMARY KEY, notified_at TIMESTAMP);")
15
+ db.execute("CREATE TABLE IF NOT EXISTS settings (key VARCHAR(50) PRIMARY KEY, value TEXT, salt TEXT);")
16
+ end
17
+
18
+ def self.execute(sql)
19
+ db.execute(sql)
20
+ end
21
+
22
+ def self.query_single_row(sql)
23
+ db.execute(sql) do |row|
24
+ return row
25
+ end
26
+ return nil
27
+ end
28
+
29
+ def self.save_setting(key, value, is_secret:)
30
+ salt = "NULL"
31
+ if is_secret
32
+ salt, encrypted = Cipher.encrypt(value)
33
+ salt = "'#{salt}'"
34
+ value = encrypted
35
+ end
36
+ db.execute("DELETE FROM settings WHERE key = '#{key}';")
37
+ db.execute("INSERT INTO settings (key, value, salt) VALUES('#{key}', '#{value}', #{salt});")
38
+ end
39
+
40
+ def self.get_setting(key)
41
+ row = query_single_row("SELECT value, salt FROM settings WHERE key = '#{key}'")
42
+ if row && row[1]
43
+ Cipher.decrypt(row[0], row[1])
44
+ elsif row
45
+ row[0]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,45 @@
1
+ require_relative "./base_api.rb"
2
+ require_relative "./models/code_change.rb"
3
+ require_relative "./models/code_change_activity.rb"
4
+
5
+ class GerritApi < BaseApi
6
+ def self.authenticate
7
+ res = post("/login/", {
8
+ body: "username=#{username}&password=#{URI.escape(password)}&rememberme=1",
9
+ headers: {
10
+ "Content-Type" => "application/x-www-form-urlencoded",
11
+ },
12
+ follow_redirects: false
13
+ })
14
+ @token = res.headers["set-cookie"].match("GerritAccount=(.*?);")[1]
15
+ raise "Your username or password is incorrect. Trying running `code_review_notifier --setup` again." if @token.nil?
16
+ end
17
+
18
+ def self.all_code_changes
19
+ wrap_with_authentication do
20
+ get("/changes/?S=0&q=is%3Aopen%20owner%3Aself%20-is%3Awip%20-is%3Aignored%20limit%3A25&q=is%3Aopen%20owner%3Aself%20is%3Awip%20limit%3A25&q=is%3Aopen%20-owner%3Aself%20-is%3Awip%20-is%3Aignored%20reviewer%3Aself%20limit%3A25&q=is%3Aclosed%20-is%3Aignored%20%28-is%3Awip%20OR%20owner%3Aself%29%20%28owner%3Aself%20OR%20reviewer%3Aself%29%20-age%3A4w%20limit%3A10&o=DETAILED_ACCOUNTS&o=MESSAGES", {
21
+ headers: {
22
+ "Cookie" => "GerritAccount=#{@token};"
23
+ }
24
+ })
25
+ end.parsed_response.flat_map { |js| js.map { |j| code_change_from_json(j) } }
26
+ end
27
+
28
+ def self.code_change_from_json(json)
29
+ code_change = CodeChange.new(json["_number"].to_s, json["owner"]["name"], json["project"], json["subject"], Time.parse(json["updated"]).to_i)
30
+ code_change.code_change_activity = json["messages"].map { |m| code_change_activity_from_json(code_change, m) }
31
+ code_change
32
+ end
33
+
34
+ def self.code_change_activity_from_json(code_change, json)
35
+ CodeChangeActivity.new(json["id"].to_s, json["author"]["name"], json["author"]["_account_id"].to_s == account_id, json["message"], Time.parse(json["date"]), code_change)
36
+ end
37
+
38
+ def self.favicon
39
+ "#{base_api_url}/favicon.ico"
40
+ end
41
+
42
+ def self.code_change_url(code_change)
43
+ "#{base_api_url}/c/#{code_change.id}"
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ require "date"
2
+ require_relative "../database.rb"
3
+
4
+ class CodeChange
5
+ attr_accessor :code_change_activity
6
+ attr_reader :id, :owner, :project, :subject, :updated_at
7
+ attr_writer :activity_from_self_at
8
+
9
+ def initialize(id, owner, project, subject, updated_at)
10
+ @id = id
11
+ @owner = owner
12
+ @project = project
13
+ @subject = subject.gsub("'", "")
14
+ @updated_at = updated_at
15
+ end
16
+
17
+ def activity_from_self_at
18
+ raise "@activity_from_self_at is not set" unless instance_variable_defined?("@activity_from_self_at")
19
+ @activity_from_self_at
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ MESSAGES_TO_IGNORE = [/Uploaded patch set 1/, /Build Started/, /owns \d+% of/]
2
+ AUTHOR_TRANSLATIONS = {
3
+ "Service Cloud Jenkins" => "Jenkins",
4
+ /\w+ Gergich \(Bot\)/ => "Gergich"
5
+ }
6
+
7
+ class CodeChangeActivity
8
+ attr_reader :id, :author, :is_self, :message, :created_at, :code_change
9
+
10
+ def initialize(id, author, is_self, message, created_at, code_change)
11
+ @id = id
12
+ @author = CodeChangeActivity.translate_author(author)
13
+ @is_self = is_self
14
+ @message = CodeChangeActivity.translate_message(message)
15
+ @created_at = created_at
16
+ @code_change = code_change
17
+ end
18
+
19
+ def notified
20
+ DB.execute("INSERT INTO code_change_activity_notified (id, notified_at) VALUES('#{id}', CURRENT_TIMESTAMP);")
21
+ end
22
+
23
+ def should_notify?
24
+ !is_self &&
25
+ code_change.activity_from_self_at && created_at > code_change.activity_from_self_at &&
26
+ MESSAGES_TO_IGNORE.none? { |m| message.match(m) } &&
27
+ !DB.query_single_row("SELECT id FROM code_change_activity_notified WHERE id = '#{id}'")
28
+ end
29
+
30
+ def self.translate_author(author)
31
+ AUTHOR_TRANSLATIONS.keys.each do |pattern|
32
+ author.sub!(pattern, AUTHOR_TRANSLATIONS[pattern])
33
+ end
34
+ author
35
+ end
36
+
37
+ def self.translate_message(message)
38
+ message.sub(/^Patch Set \d+:\s+/, "")
39
+ .gsub("'", %q(\\\\\\\\'))
40
+ .gsub("\n", " ")
41
+ .gsub(" ", " ")
42
+ .gsub(">", "")
43
+ .sub(/^\(/, "\\(")
44
+ .sub(/^\[/, "\\[")
45
+ .sub(/^-/, "\\-")
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ require_relative './api.rb'
2
+
3
+ class Notifier
4
+ def self.notify(code_change_activity)
5
+ code_change = code_change_activity.code_change
6
+ id = code_change.id
7
+ owner = code_change.owner
8
+ subject = code_change.subject
9
+
10
+ message = code_change_activity.message
11
+ author = code_change_activity.author
12
+ system("terminal-notifier -title '#{author}' -subtitle '#{owner}: #{subject}' -message '#{message}' -appIcon #{Api.current_api.favicon} -open '#{Api.current_api.code_change_url(code_change)}'")
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: code_review_notifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kyle Grinstead
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-07-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: kyleag@hey.com
15
+ executables:
16
+ - code_review_notifier
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - Brewfile
21
+ - Gemfile
22
+ - bin/code_review_notifier
23
+ - lib/api.rb
24
+ - lib/base_api.rb
25
+ - lib/cipher.rb
26
+ - lib/code_review_notifier.rb
27
+ - lib/database.rb
28
+ - lib/gerrit_api.rb
29
+ - lib/models/code_change.rb
30
+ - lib/models/code_change_activity.rb
31
+ - lib/notifier.rb
32
+ homepage: https://rubygems.org/gems/code_review_notifier
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ source_code_uri: https://github.com/MrGrinst/code_review_notifier
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.7.6.2
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Get notifications when updates happen to patch sets/pull requests!
57
+ test_files: []