code_review_notifier 0.1.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,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: []