code_review_notifier 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69b61c9f19daf4e8a85cfef15ffe035d232a5352a7240b326fadc8930b85bf9f
4
- data.tar.gz: 1e2f0b33a43e4db336fc20a2ee99a9c5e79b3da9856f48b15ac04daa87f180fd
3
+ metadata.gz: 308e660e0f67acffeca0e216ed7ed61da80fd095053c980aef42ec958cd4896e
4
+ data.tar.gz: 909a4d189606fc053d8e08684873d79492adec0f2798b4cf2686e608e55b9025
5
5
  SHA512:
6
- metadata.gz: 3fe2b9b7ef4e7b40b2e8e3581ba8ba73b7e8c93686a32c93be4f5dfd6ab9534c7aaf8016d59994ebd9bd7ec45df9260d62e91d000ba459425309b8dd2eecd5d4
7
- data.tar.gz: f79a7ab782fd1f7d73fb6ceead88144523cb39ea7cb7228563ff2393d7941bf296083af15e66ac563dd41d8ffffa4ee65fcba82781e7d59b891d4020ad93f34b
6
+ metadata.gz: e329b52cc09b78df230a7ee383d0ad796a746cc6283a1704aadf38fedcaf5590a51b338036c369ff233d60541627229bdb0bfb39e124e636660c2c7dc5249b3f
7
+ data.tar.gz: 8bddaab17fbe6ce88db2f1b96f5f1a94fa06276bf616688319b9a1ec782dc0a99500fafb9549dd44bd730752909f1c4a4f9adfebffae5d659b29bfcdbbca4740
data/Gemfile CHANGED
@@ -1,6 +1,5 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem "sqlite3"
6
- gem "httparty"
5
+ gem "rubiclifier", "1.2.0"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'code_review_notifier'
3
+ require_relative "../lib/code_review_notifier.rb"
4
4
 
5
- CodeReviewNotifier.call(ARGV)
5
+ CodeReviewNotifier.new(ARGV).call
@@ -0,0 +1,28 @@
1
+ require "rubiclifier"
2
+ require_relative "./api.rb"
3
+
4
+ class CodeChangeNotification
5
+ attr_reader :code_change_activity
6
+ private :code_change_activity
7
+
8
+ def initialize(code_change_activity)
9
+ @code_change_activity = code_change_activity
10
+ end
11
+
12
+ def send
13
+ code_change = code_change_activity.code_change
14
+ id = code_change.id
15
+ owner = code_change.owner
16
+ subject = code_change.subject
17
+
18
+ message = code_change_activity.message
19
+ author = code_change_activity.author
20
+ Rubiclifier::Notification.new(
21
+ author,
22
+ message,
23
+ "#{owner}: #{subject}",
24
+ Api.current_api.favicon,
25
+ Api.current_api.code_change_url(code_change)
26
+ ).send
27
+ end
28
+ end
@@ -1,74 +1,87 @@
1
- require "io/console"
1
+ require "rubiclifier"
2
2
  require_relative "./api.rb"
3
- require_relative "./notifier.rb"
3
+ require_relative "./code_change_notification.rb"
4
4
 
5
5
  SECONDS_BETWEEN_RUNS = 90
6
6
  SECONDS_BETWEEN_NOTIFICATIONS = 5
7
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"
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)
8
+ class CodeReviewNotifier < Rubiclifier::BaseApplication
9
+ def show_help
10
+ puts
11
+ puts("This polls for updates to patch sets/pull requests and notifies you about any relevant changes.")
12
+ puts
13
+ puts("Usage:")
14
+ puts(" code_review_notifier --help | Shows this help menu")
15
+ puts(" code_review_notifier --setup | Runs setup")
16
+ puts(" code_review_notifier | Start listening for changes (should be run in background)")
17
+ puts
18
+ exit
19
+ end
16
20
 
17
- print("What's the account username? ")
18
- DB.save_setting("username", STDIN.gets.chomp, is_secret: false)
21
+ def run_application
22
+ while true
23
+ unless Rubiclifier::IdleDetector.is_idle?
24
+ is_first_run = is_first_run?
25
+ puts
26
+ puts("Querying API...")
27
+ all_code_changes = Api.current_api.all_code_changes
28
+ puts("Checking for notifications to display...")
29
+ all_activity = []
30
+ all_code_changes.each do |cc|
31
+ cc.code_change_activity.sort! { |a, b| a.created_at <=> b.created_at }
32
+ all_activity.concat(cc.code_change_activity)
33
+ end
34
+ all_activity.select(&:should_notify?).each do |code_change_activity|
35
+ code_change_activity.notified
36
+ unless is_first_run
37
+ puts("Notifying of change!")
38
+ CodeChangeNotification.new(code_change_activity).send
39
+ sleep(SECONDS_BETWEEN_NOTIFICATIONS)
40
+ end
41
+ end
42
+ end
43
+ sleep(SECONDS_BETWEEN_RUNS)
44
+ end
45
+ end
19
46
 
20
- print("What's the account password? (hiding input) ")
21
- DB.save_setting("password", STDIN.noecho(&:gets).chomp, is_secret: true)
22
- puts
47
+ def not_setup
48
+ Rubiclifier::Notification.new(
49
+ "Missing Setup Info",
50
+ "Run `code_review_notifier --setup` to setup."
51
+ ).send
52
+ end
23
53
 
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)
54
+ def features
55
+ [
56
+ Rubiclifier::Feature::BACKGROUND,
57
+ Rubiclifier::Feature::DATABASE,
58
+ Rubiclifier::Feature::IDLE_DETECTION,
59
+ Rubiclifier::Feature::NOTIFICATIONS
60
+ ]
61
+ end
26
62
 
27
- puts("All setup!")
28
- puts
29
- puts("It's recommended that you set this up as a system service with serviceman. Check it out here: https://git.rootprojects.org/root/serviceman")
30
- puts("Set code_review_notifier to run on startup with `serviceman add --name code_review_notifier code_review_notifier`")
31
- exit
32
- end
63
+ def settings
64
+ @settings ||= [
65
+ Rubiclifier::Setting.new("base_api_url", "base URL", explanation: "e.g. https://gerrit.google.com"),
66
+ Rubiclifier::Setting.new("username", "account username"),
67
+ Rubiclifier::Setting.new("password", "account password", explanation: "input hidden", is_secret: true),
68
+ Rubiclifier::Setting.new("account_id", "account ID", explanation: -> {"check #{Api.current_api.base_api_url}/settings/"})
69
+ ]
70
+ end
33
71
 
34
- if !Api.current_api.is_setup?
35
- Notifier.notify("Missing Setup Info", "Run `code_review_notifier --setup` to setup.")
36
- puts
37
- puts("You must finish setup first by running with the `--setup` option.")
38
- puts("`code_review_notifier --setup`")
39
- exit
40
- end
72
+ def executable_name
73
+ "code_review_notifier"
41
74
  end
42
75
 
43
- def self.call(args)
44
- setup(args)
76
+ def data_directory
77
+ "~/.code_review_notifier"
78
+ end
45
79
 
46
- while true
47
- is_first_run = is_first_run?
48
- puts
49
- puts("Querying API...")
50
- all_code_changes = Api.current_api.all_code_changes
51
- puts("Checking for notifications to display...")
52
- all_activity = []
53
- all_code_changes.each do |cc|
54
- activity_for_code_change = cc.code_change_activity
55
- activity_for_code_change.sort! { |a, b| a.created_at <=> b.created_at }
56
- cc.activity_from_self_at = activity_for_code_change.find { |a| a.is_self }&.created_at
57
- all_activity.concat(activity_for_code_change)
58
- end
59
- all_activity.select(&:should_notify?).each do |code_change_activity|
60
- code_change_activity.notified
61
- unless is_first_run
62
- puts("Notifying of change!")
63
- Notifier.notify_about_code_change(code_change_activity)
64
- sleep(SECONDS_BETWEEN_NOTIFICATIONS)
65
- end
66
- end
67
- sleep(SECONDS_BETWEEN_RUNS)
68
- end
80
+ def migrations_location
81
+ "#{File.expand_path(File.dirname(__FILE__) + "/..")}/migrations.rb"
69
82
  end
70
83
 
71
- def self.is_first_run?
72
- DB.query_single_row("SELECT id FROM code_change_activity_notified;").nil?
84
+ def is_first_run?
85
+ Rubiclifier::DB.query_single_row("SELECT id FROM code_change_activity_notified;").nil?
73
86
  end
74
87
  end
@@ -1,10 +1,15 @@
1
- require_relative "./base_api.rb"
1
+ require "rubiclifier"
2
2
  require_relative "./models/code_change.rb"
3
3
  require_relative "./models/code_change_activity.rb"
4
4
 
5
- class GerritApi < BaseApi
6
- def self.authenticate
7
- @token = nil
5
+ class HTTParty::Parser
6
+ def json
7
+ JSON.parse(body.gsub(")]}'", ""))
8
+ end
9
+ end
10
+
11
+ class GerritApi < Rubiclifier::BaseApi
12
+ def self.login_and_get_api_token
8
13
  res = post("/login/", {
9
14
  body: "username=#{username}&password=#{URI.escape(password)}&rememberme=1",
10
15
  headers: {
@@ -13,22 +18,23 @@ class GerritApi < BaseApi
13
18
  follow_redirects: false
14
19
  })
15
20
  set_cookie_header = res.headers["set-cookie"] || ""
16
- token = set_cookie_header.match("GerritAccount=(.*?);")&.to_a&.fetch(1)
17
- if token.nil?
18
- Notifier.notify("Incorrect Credentials", "Trying running `code_review_notifier --setup` again.")
19
- sleep(120)
20
- exit
21
- else
22
- DB.save_setting("api_token", token, is_secret: true)
23
- token
24
- end
21
+ set_cookie_header.match("GerritAccount=(.*?);")&.to_a&.fetch(1)
22
+ end
23
+
24
+ def self.invalid_credentials_error
25
+ Rubiclifier::Notification.new(
26
+ "Incorrect Credentials",
27
+ "Trying running `code_review_notifier --setup` again."
28
+ ).send
29
+ sleep(120)
30
+ exit
25
31
  end
26
32
 
27
33
  def self.all_code_changes
28
34
  wrap_with_authentication do
29
35
  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", {
30
36
  headers: {
31
- "Cookie" => "GerritAccount=#{token};"
37
+ "Cookie" => "GerritAccount=#{api_token};"
32
38
  }
33
39
  })
34
40
  end.parsed_response.flat_map { |js| js.map { |j| code_change_from_json(j) } }
@@ -51,4 +57,24 @@ class GerritApi < BaseApi
51
57
  def self.code_change_url(code_change)
52
58
  "#{base_api_url}/c/#{code_change.id}"
53
59
  end
60
+
61
+ def self.api_token_db_key
62
+ "api_token"
63
+ end
64
+
65
+ def self.base_api_url_db_key
66
+ "base_api_url"
67
+ end
68
+
69
+ def self.username
70
+ @username ||= Rubiclifier::DB.get_setting("username")
71
+ end
72
+
73
+ def self.password
74
+ @password ||= Rubiclifier::DB.get_setting("password")
75
+ end
76
+
77
+ def self.account_id
78
+ @account_id ||= Rubiclifier::DB.get_setting("account_id")
79
+ end
54
80
  end
@@ -1,10 +1,8 @@
1
1
  require "date"
2
- require_relative "../database.rb"
3
2
 
4
3
  class CodeChange
5
4
  attr_accessor :code_change_activity
6
5
  attr_reader :id, :owner, :project, :subject, :updated_at
7
- attr_writer :activity_from_self_at
8
6
 
9
7
  def initialize(id, owner, project, subject, updated_at)
10
8
  @id = id
@@ -15,7 +13,6 @@ class CodeChange
15
13
  end
16
14
 
17
15
  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
16
+ @activity_from_self_at ||= code_change_activity.find { |a| a.is_self }&.created_at
20
17
  end
21
18
  end
@@ -1,3 +1,5 @@
1
+ require "rubiclifier"
2
+
1
3
  MESSAGES_TO_IGNORE = [/Uploaded patch set 1/, /Build Started/, /owns \d+% of/]
2
4
  AUTHOR_TRANSLATIONS = {
3
5
  "Service Cloud Jenkins" => "Jenkins",
@@ -17,14 +19,14 @@ class CodeChangeActivity
17
19
  end
18
20
 
19
21
  def notified
20
- DB.execute("INSERT INTO code_change_activity_notified (id, notified_at) VALUES('#{id}', CURRENT_TIMESTAMP);")
22
+ Rubiclifier::DB.execute("INSERT INTO code_change_activity_notified (id, notified_at) VALUES('#{id}', CURRENT_TIMESTAMP);")
21
23
  end
22
24
 
23
25
  def should_notify?
24
26
  !is_self &&
25
27
  code_change.activity_from_self_at && created_at > code_change.activity_from_self_at &&
26
28
  MESSAGES_TO_IGNORE.none? { |m| message.match(m) } &&
27
- !DB.query_single_row("SELECT id FROM code_change_activity_notified WHERE id = '#{id}'")
29
+ !Rubiclifier::DB.query_single_row("SELECT id FROM code_change_activity_notified WHERE id = '#{id}'")
28
30
  end
29
31
 
30
32
  def self.translate_author(author)
@@ -0,0 +1,8 @@
1
+ migrations = []
2
+ migrations << <<SQL
3
+ CREATE TABLE IF NOT EXISTS code_change_activity_notified (
4
+ id VARCHAR(50) PRIMARY KEY,
5
+ notified_at TIMESTAMP
6
+ );
7
+ SQL
8
+ migrations
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: code_review_notifier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyle Grinstead
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-08 00:00:00.000000000 Z
11
+ date: 2020-07-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: kyleag@hey.com
@@ -17,24 +17,22 @@ executables:
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
- - Brewfile
21
20
  - Gemfile
22
21
  - bin/code_review_notifier
23
22
  - lib/api.rb
24
- - lib/base_api.rb
25
- - lib/cipher.rb
23
+ - lib/code_change_notification.rb
26
24
  - lib/code_review_notifier.rb
27
- - lib/database.rb
28
25
  - lib/gerrit_api.rb
29
26
  - lib/models/code_change.rb
30
27
  - lib/models/code_change_activity.rb
31
- - lib/notifier.rb
28
+ - migrations.rb
32
29
  homepage: https://rubygems.org/gems/code_review_notifier
33
30
  licenses:
34
31
  - MIT
35
32
  metadata:
36
33
  source_code_uri: https://github.com/MrGrinst/code_review_notifier
37
- post_install_message:
34
+ post_install_message: "\n\e[32mThanks for installing code_review_notifier!\e[0m\n\e[32mSet
35
+ it up by running `\e[0mcode_review_notifier --setup\e[32m`\e[0m\n\n"
38
36
  rdoc_options: []
39
37
  require_paths:
40
38
  - lib
data/Brewfile DELETED
@@ -1,2 +0,0 @@
1
- brew 'sqlite'
2
- brew 'terminal-notifier'
@@ -1,66 +0,0 @@
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.token
14
- @token ||= DB.get_setting("api_token") || self.authenticate
15
- end
16
-
17
- def self.authenticate
18
- raise NotImplementedError
19
- end
20
-
21
- def self.all_code_changes
22
- raise NotImplementedError
23
- end
24
-
25
- def self.favicon
26
- raise NotImplementedError
27
- end
28
-
29
- def self.code_change_url(code_change)
30
- raise NotImplementedError
31
- end
32
-
33
- def self.wrap_with_authentication(&block)
34
- res = block.call
35
- if res.code == 401 || res.code == 403
36
- self.authenticate
37
- block.call
38
- else
39
- res
40
- end
41
- end
42
-
43
- def self.is_setup?
44
- base_api_url && username && password && account_id
45
- end
46
-
47
- def self.base_api_url
48
- @base_api_url ||= begin
49
- url = DB.get_setting("base_api_url")
50
- base_uri(url)
51
- url.chomp("/")
52
- end
53
- end
54
-
55
- def self.username
56
- @username ||= DB.get_setting("username")
57
- end
58
-
59
- def self.password
60
- @password ||= DB.get_setting("password")
61
- end
62
-
63
- def self.account_id
64
- @account_id ||= DB.get_setting("account_id")
65
- end
66
- end
@@ -1,31 +0,0 @@
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
@@ -1,48 +0,0 @@
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
@@ -1,31 +0,0 @@
1
- require_relative './api.rb'
2
-
3
- class Notifier
4
- def self.notify_about_code_change(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
- notify(author, message, "#{owner}: #{subject}", Api.current_api.favicon, Api.current_api.code_change_url(code_change))
13
- end
14
-
15
- def self.notify(title, message, subtitle = nil, icon = nil, url = nil)
16
- args = {
17
- "title" => title,
18
- "message" => message,
19
- "subtitle" => subtitle,
20
- "appIcon" => icon,
21
- "open" => url
22
- }
23
- all_args = args.keys.reduce("") do |arg_string, key|
24
- if args[key]
25
- arg_string += " -#{key} '#{args[key]}'"
26
- end
27
- arg_string
28
- end
29
- system("/usr/local/bin/terminal-notifier #{all_args}")
30
- end
31
- end