smart_todo 1.6.0 → 1.8.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 +4 -4
- data/.github/workflows/build.yml +1 -1
- data/.github/workflows/rubocop.yml +1 -2
- data/.rubocop.yml +0 -1
- data/.ruby-version +1 -0
- data/Gemfile.lock +18 -15
- data/bin/profile +16 -0
- data/dev.yml +1 -1
- data/lib/smart_todo/cli.rb +46 -12
- data/lib/smart_todo/comment_parser.rb +71 -0
- data/lib/smart_todo/dispatchers/base.rb +1 -1
- data/lib/smart_todo/events.rb +155 -11
- data/lib/smart_todo/http_client_builder.rb +19 -0
- data/lib/smart_todo/slack_client.rb +19 -28
- data/lib/smart_todo/todo.rb +84 -0
- data/lib/smart_todo/version.rb +1 -1
- data/lib/smart_todo.rb +4 -14
- data/lib/smart_todo_cop.rb +4 -4
- data/smart_todo.gemspec +1 -1
- metadata +13 -16
- data/lib/smart_todo/events/date.rb +0 -28
- data/lib/smart_todo/events/gem_bump.rb +0 -57
- data/lib/smart_todo/events/gem_release.rb +0 -69
- data/lib/smart_todo/events/issue_close.rb +0 -113
- data/lib/smart_todo/events/ruby_version.rb +0 -36
- data/lib/smart_todo/parser/comment_parser.rb +0 -71
- data/lib/smart_todo/parser/metadata_parser.rb +0 -124
- data/lib/smart_todo/parser/todo_node.rb +0 -43
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7cac40289c5fb2fdf4bb143ee7cca9b1c74ce41c7e1fa426f7efc17364a2683
|
4
|
+
data.tar.gz: c93946f4131f64068e9c249c760829a33f3e88ae669f322bb00ea0647f68eb20
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6a84fd8bd731f7b31fe0033be57a50ba018af724ae29cfa16a4c9a0a96766e6335ee1c1145976002519aff1f69d6a28a3bf220963e42f55c28b051f02c85d80
|
7
|
+
data.tar.gz: 562151dd8c4fc2b8d55d72c004a701ef2c10f01d3e84f632b1a945ee969469e7dffd5473c7f109548ca105175b1a893f1e039a9ee091b3cb8034af7e47116082
|
data/.github/workflows/build.yml
CHANGED
data/.rubocop.yml
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.0
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
smart_todo (1.
|
5
|
-
|
4
|
+
smart_todo (1.8.0)
|
5
|
+
prism (~> 0.15)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
@@ -13,34 +13,37 @@ GEM
|
|
13
13
|
crack (0.4.5)
|
14
14
|
rexml
|
15
15
|
hashdiff (1.0.1)
|
16
|
-
json (2.
|
16
|
+
json (2.7.1)
|
17
|
+
language_server-protocol (3.17.0.3)
|
17
18
|
minitest (5.14.4)
|
18
|
-
parallel (1.
|
19
|
-
parser (3.
|
19
|
+
parallel (1.24.0)
|
20
|
+
parser (3.3.0.5)
|
20
21
|
ast (~> 2.4.1)
|
21
22
|
racc
|
23
|
+
prism (0.17.0)
|
22
24
|
public_suffix (4.0.6)
|
23
|
-
racc (1.7.
|
25
|
+
racc (1.7.3)
|
24
26
|
rainbow (3.1.1)
|
25
27
|
rake (13.0.6)
|
26
|
-
regexp_parser (2.
|
27
|
-
rexml (3.2.
|
28
|
-
rubocop (1.
|
28
|
+
regexp_parser (2.9.0)
|
29
|
+
rexml (3.2.6)
|
30
|
+
rubocop (1.62.1)
|
29
31
|
json (~> 2.3)
|
32
|
+
language_server-protocol (>= 3.17.0)
|
30
33
|
parallel (~> 1.10)
|
31
|
-
parser (>= 3.
|
34
|
+
parser (>= 3.3.0.2)
|
32
35
|
rainbow (>= 2.2.2, < 4.0)
|
33
36
|
regexp_parser (>= 1.8, < 3.0)
|
34
37
|
rexml (>= 3.2.5, < 4.0)
|
35
|
-
rubocop-ast (>= 1.
|
38
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
36
39
|
ruby-progressbar (~> 1.7)
|
37
40
|
unicode-display_width (>= 2.4.0, < 3.0)
|
38
|
-
rubocop-ast (1.
|
39
|
-
parser (>= 3.
|
41
|
+
rubocop-ast (1.31.2)
|
42
|
+
parser (>= 3.3.0.4)
|
40
43
|
rubocop-shopify (2.14.0)
|
41
44
|
rubocop (~> 1.51)
|
42
45
|
ruby-progressbar (1.13.0)
|
43
|
-
unicode-display_width (2.
|
46
|
+
unicode-display_width (2.5.0)
|
44
47
|
webmock (3.11.2)
|
45
48
|
addressable (>= 2.3.6)
|
46
49
|
crack (>= 0.3.2)
|
@@ -58,4 +61,4 @@ DEPENDENCIES
|
|
58
61
|
webmock
|
59
62
|
|
60
63
|
BUNDLED WITH
|
61
|
-
2.
|
64
|
+
2.5.7
|
data/bin/profile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "smart_todo"
|
6
|
+
|
7
|
+
class NullDispatcher < SmartTodo::Dispatchers::Base
|
8
|
+
class << self
|
9
|
+
def validate_options!(_); end
|
10
|
+
end
|
11
|
+
|
12
|
+
def dispatch
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
exit SmartTodo::CLI.new(NullDispatcher).run
|
data/dev.yml
CHANGED
data/lib/smart_todo/cli.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "optionparser"
|
4
|
+
require "etc"
|
4
5
|
|
5
6
|
module SmartTodo
|
6
7
|
# This class is the entrypoint of the SmartTodo library and is responsible
|
7
8
|
# to retrieve the command line options as well as iterating over each files/directories
|
8
9
|
# to run the +CommentParser+ on.
|
9
10
|
class CLI
|
10
|
-
def initialize
|
11
|
+
def initialize(dispatcher = nil)
|
11
12
|
@options = {}
|
12
13
|
@errors = []
|
14
|
+
@dispatcher = dispatcher
|
13
15
|
end
|
14
16
|
|
15
17
|
# @param args [Array<String>]
|
@@ -19,15 +21,18 @@ module SmartTodo
|
|
19
21
|
|
20
22
|
paths << "." if paths.empty?
|
21
23
|
|
24
|
+
comment_parser = CommentParser.new
|
22
25
|
paths.each do |path|
|
23
|
-
normalize_path(path).each do |
|
24
|
-
parse_file(
|
26
|
+
normalize_path(path).each do |filepath|
|
27
|
+
comment_parser.parse_file(filepath)
|
25
28
|
|
26
29
|
$stdout.print(".")
|
27
30
|
$stdout.flush
|
28
31
|
end
|
29
32
|
end
|
30
33
|
|
34
|
+
process_dispatches(process_todos(comment_parser.todos))
|
35
|
+
|
31
36
|
if @errors.empty?
|
32
37
|
0
|
33
38
|
else
|
@@ -79,14 +84,17 @@ module SmartTodo
|
|
79
84
|
end
|
80
85
|
end
|
81
86
|
|
82
|
-
|
83
|
-
|
84
|
-
|
87
|
+
def process_todos(todos)
|
88
|
+
events = Events.new
|
89
|
+
dispatches = []
|
90
|
+
|
91
|
+
todos.each do |todo|
|
85
92
|
event_message = nil
|
86
|
-
event_met =
|
87
|
-
event_message =
|
93
|
+
event_met = todo.events.find do |event|
|
94
|
+
event_message = events.public_send(event.method_name, *event.arguments)
|
88
95
|
rescue => e
|
89
|
-
message = "Error while parsing #{
|
96
|
+
message = "Error while parsing #{todo.filepath} on event `#{event.method_name}` " \
|
97
|
+
"with arguments #{event.arguments.map(&:inspect)}: " \
|
90
98
|
"#{e.message}"
|
91
99
|
|
92
100
|
@errors << message
|
@@ -94,10 +102,36 @@ module SmartTodo
|
|
94
102
|
nil
|
95
103
|
end
|
96
104
|
|
97
|
-
@errors.concat(
|
98
|
-
|
99
|
-
dispatcher.new(event_message, todo_node, file, @options).dispatch if event_met
|
105
|
+
@errors.concat(todo.errors)
|
106
|
+
dispatches << [event_message, todo] if event_met
|
100
107
|
end
|
108
|
+
|
109
|
+
dispatches
|
110
|
+
end
|
111
|
+
|
112
|
+
def process_dispatches(dispatches)
|
113
|
+
queue = Queue.new
|
114
|
+
dispatches.each { |dispatch| queue << dispatch }
|
115
|
+
|
116
|
+
thread_count = Etc.nprocessors
|
117
|
+
thread_count.times { queue << nil }
|
118
|
+
|
119
|
+
threads =
|
120
|
+
thread_count.times.map do
|
121
|
+
Thread.new do
|
122
|
+
Thread.current.abort_on_exception = true
|
123
|
+
|
124
|
+
loop do
|
125
|
+
dispatch = queue.pop
|
126
|
+
break if dispatch.nil?
|
127
|
+
|
128
|
+
(event_message, todo) = dispatch
|
129
|
+
dispatcher.new(event_message, todo, todo.filepath, @options).dispatch
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
threads.each(&:join)
|
101
135
|
end
|
102
136
|
end
|
103
137
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmartTodo
|
4
|
+
class CommentParser
|
5
|
+
attr_reader :todos
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@todos = []
|
9
|
+
end
|
10
|
+
|
11
|
+
if Prism.respond_to?(:parse_comments)
|
12
|
+
def parse(source, filepath = "-e")
|
13
|
+
parse_comments(Prism.parse_comments(source), filepath)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse_file(filepath)
|
17
|
+
parse_comments(Prism.parse_file_comments(filepath), filepath)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
def parse(source, filepath = "-e")
|
21
|
+
parse_comments(Prism.parse(source, filepath).comments, filepath)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_file(filepath)
|
25
|
+
parse_comments(Prism.parse_file(filepath).comments, filepath)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def parse(source)
|
31
|
+
parser = new
|
32
|
+
parser.parse(source)
|
33
|
+
parser.todos
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
if defined?(Prism::InlineComment)
|
40
|
+
def inline?(comment)
|
41
|
+
comment.is_a?(Prism::InlineComment)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
def inline?(comment)
|
45
|
+
comment.type == :inline
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_comments(comments, filepath)
|
50
|
+
current_todo = nil
|
51
|
+
|
52
|
+
comments.each do |comment|
|
53
|
+
next unless inline?(comment)
|
54
|
+
|
55
|
+
source = comment.location.slice
|
56
|
+
|
57
|
+
if source.match?(/^#\sTODO\(/)
|
58
|
+
todos << current_todo if current_todo
|
59
|
+
current_todo = Todo.new(source, filepath)
|
60
|
+
elsif current_todo && (indent = source[/^#(\s*)/, 1].length) && (indent - current_todo.indent == 2)
|
61
|
+
current_todo << "#{source[(indent + 1)..]}\n"
|
62
|
+
else
|
63
|
+
todos << current_todo if current_todo
|
64
|
+
current_todo = nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
todos << current_todo if current_todo
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -40,7 +40,7 @@ module SmartTodo
|
|
40
40
|
@todo_node = todo_node
|
41
41
|
@options = options
|
42
42
|
@file = file
|
43
|
-
@assignees = @todo_node.
|
43
|
+
@assignees = @todo_node.assignees
|
44
44
|
end
|
45
45
|
|
46
46
|
# This method gets called when a TODO reminder is expired and needs to be delivered.
|
data/lib/smart_todo/events.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
gem("bundler")
|
4
|
+
require "bundler"
|
5
|
+
require "net/http"
|
6
|
+
require "time"
|
7
|
+
require "json"
|
8
|
+
|
3
9
|
module SmartTodo
|
4
10
|
# This module contains all the methods accessible for SmartTodo comments.
|
5
11
|
# It is meant to be reopened by the host application in order to define
|
@@ -10,7 +16,7 @@ module SmartTodo
|
|
10
16
|
#
|
11
17
|
# @example Adding a custom event
|
12
18
|
# module SmartTodo
|
13
|
-
#
|
19
|
+
# class Events
|
14
20
|
# def trello_card_close(card)
|
15
21
|
# ...
|
16
22
|
# end
|
@@ -18,33 +24,87 @@ module SmartTodo
|
|
18
24
|
# end
|
19
25
|
#
|
20
26
|
# TODO(on: trello_card_close(381), to: 'john@example.com')
|
21
|
-
|
22
|
-
|
27
|
+
#
|
28
|
+
class Events
|
29
|
+
def initialize(now: nil, spec_set: nil, current_ruby_version: nil)
|
30
|
+
@now = now
|
31
|
+
@spec_set = spec_set
|
32
|
+
@rubygems_client = nil
|
33
|
+
@github_client = nil
|
34
|
+
@current_ruby_version = current_ruby_version
|
35
|
+
end
|
23
36
|
|
24
37
|
# Check if the +date+ is in the past
|
25
38
|
#
|
26
|
-
# @param
|
39
|
+
# @param on_date [String] a string parsable by Time.parse
|
27
40
|
# @return [false, String]
|
28
|
-
def date(
|
29
|
-
|
41
|
+
def date(on_date)
|
42
|
+
if now >= Time.parse(on_date)
|
43
|
+
"We are past the *#{on_date}* due date and " \
|
44
|
+
"your TODO is now ready to be addressed."
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
30
48
|
end
|
31
49
|
|
32
50
|
# Check if a new version of +gem_name+ was released with the +requirements+ expected
|
33
51
|
#
|
52
|
+
# @example Expecting a specific version
|
53
|
+
# gem_release('rails', '6.0')
|
54
|
+
#
|
55
|
+
# @example Expecting a version in the 5.x.x series
|
56
|
+
# gem_release('rails', '> 5.2', '< 6')
|
57
|
+
#
|
34
58
|
# @param gem_name [String]
|
35
59
|
# @param requirements [Array<String>] a list of version specifiers
|
36
60
|
# @return [false, String]
|
37
61
|
def gem_release(gem_name, *requirements)
|
38
|
-
|
62
|
+
response = rubygems_client.get("/api/v1/versions/#{gem_name}.json")
|
63
|
+
|
64
|
+
if response.code_type < Net::HTTPClientError
|
65
|
+
"The gem *#{gem_name}* doesn't seem to exist, I can't determine if " \
|
66
|
+
"your TODO is ready to be addressed."
|
67
|
+
else
|
68
|
+
requirement = Gem::Requirement.new(requirements)
|
69
|
+
version = JSON.parse(response.body).find { |gem| requirement.satisfied_by?(Gem::Version.new(gem["number"])) }
|
70
|
+
|
71
|
+
if version
|
72
|
+
"The gem *#{gem_name}* was released to version *#{version["number"]}* and " \
|
73
|
+
"your TODO is now ready to be addressed."
|
74
|
+
else
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
39
78
|
end
|
40
79
|
|
41
80
|
# Check if +gem_name+ was bumped to the +requirements+ expected
|
42
81
|
#
|
82
|
+
# @example Expecting a specific version
|
83
|
+
# gem_bump('rails', '6.0')
|
84
|
+
#
|
85
|
+
# @example Expecting a version in the 5.x.x series
|
86
|
+
# gem_bump('rails', '> 5.2', '< 6')
|
87
|
+
#
|
43
88
|
# @param gem_name [String]
|
44
89
|
# @param requirements [Array<String>] a list of version specifiers
|
45
90
|
# @return [false, String]
|
46
91
|
def gem_bump(gem_name, *requirements)
|
47
|
-
|
92
|
+
specs = spec_set[gem_name]
|
93
|
+
|
94
|
+
if specs.empty?
|
95
|
+
"The gem *#{gem_name}* is not in your dependencies, I can't determine if " \
|
96
|
+
"your TODO is ready to be addressed."
|
97
|
+
else
|
98
|
+
requirement = Gem::Requirement.new(requirements)
|
99
|
+
version = specs.first.version
|
100
|
+
|
101
|
+
if requirement.satisfied_by?(version)
|
102
|
+
"The gem *#{gem_name}* was updated to version *#{version}* and " \
|
103
|
+
"your TODO is now ready to be addressed."
|
104
|
+
else
|
105
|
+
false
|
106
|
+
end
|
107
|
+
end
|
48
108
|
end
|
49
109
|
|
50
110
|
# Check if the issue +issue_number+ is closed
|
@@ -54,7 +114,22 @@ module SmartTodo
|
|
54
114
|
# @param issue_number [String, Integer]
|
55
115
|
# @return [false, String]
|
56
116
|
def issue_close(organization, repo, issue_number)
|
57
|
-
|
117
|
+
headers = github_headers(organization, repo)
|
118
|
+
response = github_client.get("/repos/#{organization}/#{repo}/issues/#{issue_number}", headers)
|
119
|
+
|
120
|
+
if response.code_type < Net::HTTPClientError
|
121
|
+
<<~EOM
|
122
|
+
I can't retrieve the information from the issue *#{issue_number}* in the *#{organization}/#{repo}* repository.
|
123
|
+
|
124
|
+
If the repository is a private one, make sure to export the `#{GITHUB_TOKEN}`
|
125
|
+
environment variable with a correct GitHub token.
|
126
|
+
EOM
|
127
|
+
elsif JSON.parse(response.body)["state"] == "closed"
|
128
|
+
"The issue https://github.com/#{organization}/#{repo}/issues/#{issue_number} is now closed, " \
|
129
|
+
"your TODO is ready to be addressed."
|
130
|
+
else
|
131
|
+
false
|
132
|
+
end
|
58
133
|
end
|
59
134
|
|
60
135
|
# Check if the pull request +pr_number+ is closed
|
@@ -64,7 +139,22 @@ module SmartTodo
|
|
64
139
|
# @param pr_number [String, Integer]
|
65
140
|
# @return [false, String]
|
66
141
|
def pull_request_close(organization, repo, pr_number)
|
67
|
-
|
142
|
+
headers = github_headers(organization, repo)
|
143
|
+
response = github_client.get("/repos/#{organization}/#{repo}/pulls/#{pr_number}", headers)
|
144
|
+
|
145
|
+
if response.code_type < Net::HTTPClientError
|
146
|
+
<<~EOM
|
147
|
+
I can't retrieve the information from the PR *#{pr_number}* in the *#{organization}/#{repo}* repository.
|
148
|
+
|
149
|
+
If the repository is a private one, make sure to export the `#{GITHUB_TOKEN}`
|
150
|
+
environment variable with a correct GitHub token.
|
151
|
+
EOM
|
152
|
+
elsif JSON.parse(response.body)["state"] == "closed"
|
153
|
+
"The pull request https://github.com/#{organization}/#{repo}/pull/#{pr_number} is now closed, " \
|
154
|
+
"your TODO is ready to be addressed."
|
155
|
+
else
|
156
|
+
false
|
157
|
+
end
|
68
158
|
end
|
69
159
|
|
70
160
|
# Check if the installed ruby version meets requirements.
|
@@ -72,7 +162,61 @@ module SmartTodo
|
|
72
162
|
# @param requirements [Array<String>] a list of version specifiers
|
73
163
|
# @return [false, String]
|
74
164
|
def ruby_version(*requirements)
|
75
|
-
|
165
|
+
requirement = Gem::Requirement.new(requirements)
|
166
|
+
|
167
|
+
if requirement.satisfied_by?(current_ruby_version)
|
168
|
+
"The currently installed version of Ruby #{current_ruby_version} is #{requirement}."
|
169
|
+
else
|
170
|
+
false
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def now
|
177
|
+
@now ||= Time.now
|
178
|
+
end
|
179
|
+
|
180
|
+
def spec_set
|
181
|
+
@spec_set ||= Bundler.load.specs
|
182
|
+
end
|
183
|
+
|
184
|
+
def rubygems_client
|
185
|
+
@rubygems_client ||= HttpClientBuilder.build("rubygems.org")
|
186
|
+
end
|
187
|
+
|
188
|
+
def github_client
|
189
|
+
@github_client ||= HttpClientBuilder.build("api.github.com")
|
190
|
+
end
|
191
|
+
|
192
|
+
def github_headers(organization, repo)
|
193
|
+
headers = { "Accept" => "application/vnd.github.v3+json" }
|
194
|
+
|
195
|
+
token = github_authorization_token(organization, repo)
|
196
|
+
headers["Authorization"] = "token #{token}" if token
|
197
|
+
|
198
|
+
headers
|
199
|
+
end
|
200
|
+
|
201
|
+
GITHUB_TOKEN = "SMART_TODO_GITHUB_TOKEN"
|
202
|
+
|
203
|
+
# @return [String, nil]
|
204
|
+
def github_authorization_token(organization, repo)
|
205
|
+
organization_name = organization.upcase.gsub(/[^A-Z0-9]/, "_")
|
206
|
+
repo_name = repo.upcase.gsub(/[^A-Z0-9]/, "_")
|
207
|
+
|
208
|
+
[
|
209
|
+
"#{GITHUB_TOKEN}__#{organization_name}__#{repo_name}",
|
210
|
+
"#{GITHUB_TOKEN}__#{organization_name}",
|
211
|
+
GITHUB_TOKEN,
|
212
|
+
].find do |key|
|
213
|
+
token = ENV[key]
|
214
|
+
break token unless token.nil? || token.empty?
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def current_ruby_version
|
219
|
+
@current_ruby_version ||= Gem::Version.new(RUBY_VERSION)
|
76
220
|
end
|
77
221
|
end
|
78
222
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
|
5
|
+
module SmartTodo
|
6
|
+
# @api private
|
7
|
+
class HttpClientBuilder
|
8
|
+
class << self
|
9
|
+
def build(endpoint)
|
10
|
+
Net::HTTP.new(endpoint, Net::HTTP.https_default_port).tap do |client|
|
11
|
+
client.use_ssl = true
|
12
|
+
client.read_timeout = 30
|
13
|
+
client.ssl_timeout = 15
|
14
|
+
client.max_retries = 2
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -26,11 +26,7 @@ module SmartTodo
|
|
26
26
|
# @param slack_token [String]
|
27
27
|
def initialize(slack_token)
|
28
28
|
@slack_token = slack_token
|
29
|
-
@client =
|
30
|
-
client.use_ssl = true
|
31
|
-
client.read_timeout = 30
|
32
|
-
client.ssl_timeout = 15
|
33
|
-
end
|
29
|
+
@client = HttpClientBuilder.build("slack.com")
|
34
30
|
end
|
35
31
|
|
36
32
|
# Retrieve the Slack ID of a user from his email
|
@@ -43,9 +39,9 @@ module SmartTodo
|
|
43
39
|
#
|
44
40
|
# @see https://api.slack.com/methods/users.lookupByEmail
|
45
41
|
def lookup_user_by_email(email)
|
46
|
-
headers =
|
47
|
-
|
48
|
-
request
|
42
|
+
headers = default_headers.merge("Content-Type" => "application/x-www-form-urlencoded")
|
43
|
+
request = Net::HTTP::Get.new("/api/users.lookupByEmail?email=#{CGI.escape(email)}", headers)
|
44
|
+
dispatch(request)
|
49
45
|
end
|
50
46
|
|
51
47
|
# Send a message to a Slack channel or to a user
|
@@ -59,7 +55,10 @@ module SmartTodo
|
|
59
55
|
#
|
60
56
|
# @see https://api.slack.com/methods/chat.postMessage
|
61
57
|
def post_message(channel, text)
|
62
|
-
|
58
|
+
headers = default_headers
|
59
|
+
request = Net::HTTP::Post.new("/api/chat.postMessage", headers)
|
60
|
+
request.body = JSON.dump(channel: channel, text: text)
|
61
|
+
dispatch(request)
|
63
62
|
end
|
64
63
|
|
65
64
|
private
|
@@ -71,27 +70,19 @@ module SmartTodo
|
|
71
70
|
#
|
72
71
|
# @raise [Net::HTTPError] in case the request to Slack failed
|
73
72
|
# @raise [SlackClient::Error] in case Slack returns a { ok: false } in the body
|
74
|
-
def request
|
75
|
-
response =
|
76
|
-
|
77
|
-
@client.public_send(method, endpoint, data, default_headers.merge(headers))
|
78
|
-
else
|
79
|
-
@client.public_send(method, endpoint, default_headers.merge(headers))
|
80
|
-
end
|
73
|
+
def dispatch(request, max_attempts = 5)
|
74
|
+
response = @client.request(request)
|
75
|
+
attempts = 1
|
81
76
|
|
82
|
-
|
83
|
-
|
77
|
+
while response.is_a?(Net::HTTPTooManyRequests) && attempts < max_attempts
|
78
|
+
sleep([Integer(response["Retry-After"]), 600].min)
|
79
|
+
response = @client.request(request)
|
80
|
+
attempts += 1
|
81
|
+
end
|
84
82
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
# (Net::HTTPOK, Net::HTTPNotFound ...)
|
89
|
-
# @return [Hash]
|
90
|
-
#
|
91
|
-
# @raise [Net::HTTPError] in case the request to Slack failed
|
92
|
-
# @raise [SlackClient::Error] in case Slack returns a { ok: false } in the body
|
93
|
-
def slack_response!(response)
|
94
|
-
raise(Net::HTTPError.new("Request to slack failed", response)) unless response.code_type < Net::HTTPSuccess
|
83
|
+
unless response.code_type < Net::HTTPSuccess
|
84
|
+
raise(Net::HTTPError.new("Request to slack failed", response))
|
85
|
+
end
|
95
86
|
|
96
87
|
body = JSON.parse(response.body)
|
97
88
|
|