ruboty-redmine 0.1.7 → 1.0.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
  SHA1:
3
- metadata.gz: 0455311b6497c40a5a2517cc557aee54f4abb381
4
- data.tar.gz: a72b526d26bee5e47d4f8bacd32537c305ceb23f
3
+ metadata.gz: d200e7bc660e4bcab752cb57f74289d882679bbd
4
+ data.tar.gz: 63326c9214311143f44365859121ed79ea245869
5
5
  SHA512:
6
- metadata.gz: 658d9e332aba5e69aed4063750c39b8d5dda2db4492aaca89301c6480a02d3fb3392df8389ada2504e98d18ae42779a1a7869f8eb8918860dcf112abe883e8ba
7
- data.tar.gz: e6a819fe5bc996352450c4d2f05447eea06d0b0252c58849ee753b7afe92ff0c31d566e46f98f34bdab78666c3b81d2422fa413408c595fd34aff7d5ed045b66
6
+ metadata.gz: ce30f6536f2e2d126180252b101371456cdd181ba65863f4ebfe4f26850fea54a7e4c54dc084b22a09fb4c1ca02b70453b2c00b632b8e355270a6c04a4bf0ab0
7
+ data.tar.gz: c4e8e22e7f4af6193391bcab9a0a011eed9e3d444583d2eef269028710f7d4f48a6df01f59fbc3a30a1c82be447aa91833b4713751f639797927694c4a277079
data/README.md CHANGED
@@ -2,64 +2,27 @@
2
2
 
3
3
  Redmine plugin for Ruboty
4
4
 
5
- This plugin is tested only with ruboty-hipchat.
5
+ **This plugin currently supports only Slack**
6
6
 
7
7
  ## Available commands
8
8
 
9
- ### create issue
10
-
11
- ```
12
- > ruboty create issue "subject" "Mobile" project "Feature" tracker
13
- ```
14
-
15
- ### watch issues
16
-
17
- ```
18
- > ruboty watch redmine issues in "Feature" tracker of "Mobile" project
19
- ```
20
-
21
- You will get notifications:
22
-
23
9
  ```
24
- New Issue of Feature in Mobile project
25
- -> Awesome search feature
26
- -> https://redmine.example.com/issues/123
10
+ # Creating an issue
11
+ ruboty create redmine YOURTRACKER issue in YOURPROJECT subject of your issue
12
+ # Automatic assign
13
+ ruboty assign YOURTRACKER issues in YOURPROJECT to @MENTION_NAME_OF_ASSIGNEE REDMINE_USER_ID_OF_ASSIGNEE and notify to #CHANNEL_TO_BE_NOTIFIED
14
+ # List assignees
15
+ ruboty list redmine assignees
16
+ # Remove assignee
17
+ ruboty remove redmine assignee ID_GOT_FROM_LIST_COMMAND
18
+ # Pause assigning temporalily
19
+ ruboty pause assigning redmine issues to @MENTION_NAME_OF_ASSIGNEE for 1w1d1h1m1s
20
+ # Unpause assigning
21
+ ruboty unpause assigning redmien issues to @MENTION_NAME_OF_ASSIGNEE
22
+ # List paused assignees
23
+ ruboty list paused assignees
27
24
  ```
28
25
 
29
- You can list and stop watching issues:
30
-
31
- ```
32
- > ruboty list watching redmine issues
33
- #1 Feature tracker in Mobile project and assign to [] (your_chat_room)
34
- > ruboty stop watching redmine issues 1
35
- ```
36
-
37
- ### watch and assign issues
38
-
39
- First, associate Redmine user ID with your name in chat:
40
-
41
- ```
42
- > ruboty redmine user #123 is @bob
43
- > ruboty redmine user #456 is @alice
44
- ```
45
-
46
- Register tracker:
47
-
48
- ```
49
- > ruboty watch redmine issues in "Feature" tracker of "Mobile" project and assign to 123,456
50
- ```
51
-
52
- You will get notifications and the issue is assigned automatically:
53
-
54
- ```
55
- New Issue of Feature in Mobile project
56
- -> Awesome search feature
57
- -> Assigned to @bob
58
- -> https://redmine.example.com/issues/123
59
- ```
60
-
61
- The assignee will be elected by round-robin.
62
-
63
26
  ## Installation
64
27
 
65
28
  Add this line to your application's Gemfile:
@@ -1,7 +1,9 @@
1
+ require 'slack-notifier'
2
+
1
3
  module Ruboty
2
4
  module Handlers
3
5
  class Redmine < Base
4
- NAMESPACE = 'redmine'
6
+ NAMESPACE = 'redmine_v1'
5
7
 
6
8
  env :REDMINE_URL, 'Redmine url (e.g. http://your-redmine)', optional: false
7
9
  env :REDMINE_API_KEY, 'Redmine REST API key', optional: false
@@ -9,217 +11,223 @@ module Ruboty
9
11
  env :REDMINE_BASIC_AUTH_PASSWORD, 'Basic Auth Password', optional: true
10
12
  env :REDMINE_CHECK_INTERVAL, 'Interval to check new issues', optional: true
11
13
  env :REDMINE_HTTP_PROXY, 'HTTP proxy', optional: true
14
+ env :SLACK_WEBHOOK_URL, 'Slack webhook URL', optional: false
12
15
 
13
16
  on(
14
- /create issue (?<rest>.+)/,
17
+ /create redmine (?<tracker>[^ ]+) issue in (?<project>[^ ]+) (?<subject>.+)/,
15
18
  name: 'create_issue',
16
- description: 'Create a new issue'
19
+ description: 'Create a new Redmine issue'
17
20
  )
18
21
 
19
22
  on(
20
- /watch redmine issues in "(?<tracker>[^"]+)" tracker of "(?<project>[^"]+)" project( and assign to (?<assignees>[\d,]+)|)/,
21
- name: 'watch_issues',
22
- description: 'Watch issues'
23
+ /assign redmine (?<tracker>[^ ]+) issues in (?<project>[^ ]+) to (?<mention_name>[^ ]+) (?<redmine_user_id>\d+) and notify to (?<channel>[^ ]+)/,
24
+ name: 'assign_issues',
25
+ description: 'Assign Redmine issues when created'
23
26
  )
24
27
 
25
28
  on(
26
- /list watching redmine issues/,
27
- name: 'list_watching',
28
- description: 'List watching issues'
29
+ /list redmine assignees/,
30
+ name: 'list_assignees',
31
+ description: 'List rules to assign Redmine issues',
29
32
  )
30
33
 
31
34
  on(
32
- /stop watching redmine issues (?<id>\d+)/,
33
- name: 'stop_watching',
34
- description: 'Stop watching issues',
35
+ /remove redmine assignee (?<id>\d+)/,
36
+ name: 'remove_redmine_assignee',
37
+ description: 'Stop assigning Redmine issues'
35
38
  )
36
39
 
37
40
  on(
38
- /redmine user #(?<redmine_id>\d+) is @(?<chat_name>.+)/,
39
- name: 'associate_user',
40
- description: 'Associate redmine_id with chat_name',
41
+ /pause assigning redmine issues to (?<mention_name>[^ ]+) for (?<duration>[^ ]+)/,
42
+ name: 'pause_assigning',
43
+ description: 'Pause assigning Redmine issues',
41
44
  )
42
45
 
43
46
  on(
44
- /redmine stop assigning to (?<user>.+)/,
45
- name: 'stop_assigning',
46
- description: 'Stop assigning issues to the user',
47
+ /unpause assigning redmine issues to (?<mention_name>[^ ]+)/,
48
+ name: 'unpause_assigning',
49
+ description: 'Unpause assigning Redmine issues',
47
50
  )
48
51
 
49
52
  on(
50
- /redmine start assigning to (?<user>.+)/,
51
- name: 'start_assigning',
52
- description: 'Start assigning issues to the user',
53
- )
54
-
55
- on(
56
- /redmine list absent users/,
57
- name: 'list_absent_users',
58
- description: 'List absent users',
53
+ /list paused assignees/,
54
+ name: 'list_paused_assignees',
55
+ description: 'List paused Redmine assignees',
59
56
  )
60
57
 
61
58
  def initialize(*args)
62
59
  super
63
-
64
- start_to_watch_issues
65
60
  end
66
61
 
67
62
  def create_issue(message)
68
63
  from_name = message.original[:from_name]
69
64
 
70
- words = parse_arg(message[:rest])
71
65
  req = {}
72
- req[:subject] = "#{words.shift} (from #{from_name})"
73
-
74
- words.each_with_index do |word, i|
75
- next if i == 0
76
-
77
- arg = words[i - 1]
78
-
79
- case word
80
- when 'project'
81
- project = redmine.find_project(arg)
82
-
83
- unless project
84
- message.reply("Project '#{arg}' is not found.")
85
- return
86
- end
87
-
88
- req[:project] = project
89
- when 'tracker'
90
- tracker = redmine.find_tracker(arg)
66
+ req[:subject] = "#{message[:subject]} (from #{from_name})"
91
67
 
92
- unless tracker
93
- message.reply("Tracker '#{arg}' is not found.")
94
- return
95
- end
96
-
97
- req[:tracker] = tracker
98
- end
68
+ project = redmine.find_project(message[:project])
69
+ unless project
70
+ message.reply("Project '#{message[:project]}' is not found.")
71
+ return
99
72
  end
73
+ req[:project] = project
100
74
 
101
- unless req.has_key?(:project)
102
- message.reply("Project must be specified.")
75
+ tracker = redmine.find_tracker(message[:tracker])
76
+ unless tracker
77
+ message.reply("Tracker '#{message[:tracker]}' is not found.")
103
78
  return
104
79
  end
80
+ req[:tracker] = tracker
105
81
 
106
82
  issue = redmine.create_issue(req)
83
+ Ruboty.logger.debug("Created new issue: #{issue.inspect}")
107
84
  message.reply("Issue created: #{redmine.url_for_issue(issue)}")
108
- end
109
85
 
110
- def watch_issues(message)
111
- if message[:assignees]
112
- assignees = message[:assignees].split(',').map(&:to_i)
113
- else
114
- assignees = []
86
+ rules = assignees.select do |r|
87
+ paused = active_paused_assignees.find do |a|
88
+ a[:mention_name] == r[:mention_name]
89
+ end
90
+ next false if paused
91
+
92
+ r[:project] == issue.project['name'] &&
93
+ r[:tracker] == issue.tracker['name']
115
94
  end
95
+ rule = rules[issue.id % rules.size]
96
+ redmine.update_issue(issue, assigned_to_id: rule[:redmine_user_id])
116
97
 
117
- id = if watches.empty?
118
- 1
119
- else
120
- watches.last['id'] + 1
121
- end
98
+ notify_slack(rule[:notify_to], <<-EOTEXT)
99
+ New Issue of #{issue.tracker['name']} in #{issue.project['name']} project
100
+ -> #{issue.subject}
101
+ -> Assigned to @#{rule[:mention_name]}
102
+ -> #{redmine.url_for_issue(issue)}
103
+ EOTEXT
104
+ end
122
105
 
123
- watches << message.original.except(:robot).merge(
124
- {id: id, project: message[:project], tracker: message[:tracker], assignees: assignees, assignee_index: 0}
125
- ).stringify_keys
106
+ def assignees
107
+ robot.brain.data["#{NAMESPACE}_assigns"] ||= []
108
+ end
126
109
 
127
- message.reply("Watching.")
110
+ def paused_assignees
111
+ robot.brain.data["#{NAMESPACE}_paused_assignees"] ||= []
128
112
  end
129
113
 
130
- def list_watching(message)
131
- reply = watches.map do |watch|
132
- s = "##{watch['id']} #{watch['tracker']} tracker in #{watch['project']} project"
133
- if assignees = watch['assignees']
134
- s << " and assign to #{assignees}"
135
- end
114
+ def active_paused_assignees
115
+ paused_assignees.select do |a|
116
+ Time.now < Time.at(a[:expire_at])
117
+ end
118
+ end
119
+
120
+ def assign_issues(message)
121
+ rule = {
122
+ tracker: message[:tracker],
123
+ project: message[:project],
124
+ mention_name: message[:mention_name].gsub(/\A@/, ''),
125
+ redmine_user_id: message[:redmine_user_id].gsub(/\A#/, '').to_i,
126
+ notify_to: message[:channel].gsub(/\A#/, ''),
127
+ }
128
+
129
+ assignees << rule
130
+
131
+ message.reply("Registered: #{rule}")
132
+ end
136
133
 
137
- room = case watch['type']
138
- when "groupchat"
139
- watch['from'].split('@').first
140
- when "chat"
141
- watch['from_name']
142
- else
143
- "unknown"
144
- end
134
+ def list_assignees(message)
135
+ if assignees.empty?
136
+ message.reply("No rule is found")
137
+ end
145
138
 
146
- s << " (#{room})"
139
+ reply = assignees.map do |rule|
140
+ "#{rule.object_id} #{rule}"
147
141
  end.join("\n")
148
142
 
149
143
  message.reply(reply)
150
144
  end
151
145
 
152
- def stop_watching(message)
146
+ def remove_redmine_assignee(message)
153
147
  id = message[:id].to_i
154
- watches.reject! do |watch|
155
- watch['id'] == id
148
+ rule = assignees.find {|r| r.object_id == id }
149
+ if rule
150
+ assignees.delete(rule)
151
+ message.reply("Rule #{id} is removed")
152
+ else
153
+ message.reply("The rule is not found")
156
154
  end
157
-
158
- message.reply("Stopped.")
159
155
  end
160
156
 
161
- def associate_user(message)
162
- users << {"redmine_id" => message[:redmine_id].to_i, "chat_name" => message[:chat_name]}
157
+ def pause_assigning(message)
158
+ mention_name = message[:mention_name]
159
+ duration = message[:duration]
160
+
161
+ expire_at = Time.now + parse_duration_to_sec(duration)
163
162
 
164
- message.reply("Registered.")
163
+ pause = {
164
+ mention_name: mention_name.gsub(/\A@/, ''),
165
+ expire_at: expire_at.to_i
166
+ }
167
+ paused_assignees << pause
168
+
169
+ message.reply("Paused: #{pause}")
165
170
  end
166
171
 
167
- def stop_assigning(message)
168
- u = username_to_redmine_id(message[:user])
169
- unless u
170
- message.reply("#{message[:user]} is not found")
171
- return
172
- end
172
+ def unpause_assigning(message)
173
+ mention_name = message[:mention_name].gsub(/\A@/, '')
173
174
 
174
- if absent_users.include?(u)
175
- message.reply("Assigning to #{message[:user]} is already stopped")
176
- return
175
+ prev_size = paused_assignees.size
176
+ paused_assignees.reject! do |a|
177
+ a[:mention_name] == mention_name
177
178
  end
178
179
 
179
- absent_users << u
180
- message.reply("Stop assigning issues to #{u}")
180
+ if parsed_assignees.size == prev_size
181
+ message.reply("No paused assignee is found")
182
+ else
183
+ message.reply("Unpaused")
184
+ end
181
185
  end
182
186
 
183
- def start_assigning(message)
184
- u = username_to_redmine_id(message[:user])
185
- unless u
186
- message.reply("#{message[:user]} is not found")
187
+ def list_paused_assignees(message)
188
+ if paused_assignees.empty?
189
+ message.reply("No paused assingee is found.")
187
190
  return
188
191
  end
189
192
 
190
- unless absent_users.include?(u)
191
- message.reply("Assigning to #{message[:user]} is not stopped")
192
- return
193
- end
193
+ reply = active_paused_assignees.map do |a|
194
+ "#{a[:mention_name]} (until #{Time.at(a[:expire_at])})"
195
+ end.join("\n")
194
196
 
195
- absent_users.delete(u)
196
- message.reply("Start assigning issues to #{u}")
197
+ message.reply(reply)
197
198
  end
198
199
 
199
- def list_absent_users(message)
200
- message.reply(absent_users.map {|id| redmine_id_to_username(id) }.join(", "))
200
+ def parse_duration_to_sec(d)
201
+ sum = 0
202
+ d.scan(/(\d+)([smhdw])/) do |n, u|
203
+ scale = case u
204
+ when 's'
205
+ 1
206
+ when 'm'
207
+ 60
208
+ when 'h'
209
+ 60*60
210
+ when 'd'
211
+ 24*60*60
212
+ when 'w'
213
+ 7*24*60*60
214
+ end
215
+ sum += n.to_i + scale
216
+ end
217
+ sum
201
218
  end
202
219
 
203
- private
204
-
205
- def username_to_redmine_id(username)
206
- case username
207
- when /\A\d+\z/
208
- username.to_i # redmine_id
209
- else
210
- u = users.find do |u|
211
- u['chat_name'] == username
212
- end
213
-
214
- u && u['redmine_id'].to_i
215
- end
220
+ def notify_slack(channel, message)
221
+ slack_notifier.ping(
222
+ text: message,
223
+ channel: channel,
224
+ username: 'ruboty',
225
+ link_names: '1',
226
+ )
216
227
  end
217
228
 
218
- def redmine_id_to_username(redmine_id)
219
- u = users.find do |u|
220
- u['redmine_id'] == redmine_id.to_i
221
- end
222
- u && u['chat_name']
229
+ def slack_notifier
230
+ @slack_notifier ||= Slack::Notifier.new(ENV['SLACK_WEBHOOK_URL'])
223
231
  end
224
232
 
225
233
  def redmine
@@ -231,98 +239,6 @@ module Ruboty
231
239
  http_proxy: ENV['REDMINE_HTTP_PROXY'],
232
240
  )
233
241
  end
234
-
235
- def watches
236
- robot.brain.data["#{NAMESPACE}_watches"] ||= []
237
- end
238
-
239
- def users
240
- robot.brain.data["#{NAMESPACE}_users"] ||= []
241
- end
242
-
243
- def absent_users
244
- robot.brain.data["#{NAMESPACE}_absent_users"] ||= []
245
- end
246
-
247
- def find_user_by_id(id)
248
- users.find {|user| user['redmine_id'] == id }
249
- end
250
-
251
- def parse_arg(text)
252
- text.scan(/("([^"]+)"|'([^']+)'|([^ ]+))/).map do |v|
253
- v.shift
254
- v.find {|itself| itself }
255
- end
256
- end
257
-
258
- def start_to_watch_issues
259
- thread = Thread.start do
260
- last_issues_for_watch = {}
261
-
262
- while true
263
- sleep (ENV['REDMINE_CHECK_INTERVAL'] || 30).to_i
264
- watches.each do |watch|
265
- project = redmine.find_project(watch['project'])
266
- tracker = redmine.find_tracker(watch['tracker'])
267
-
268
- issues = redmine.issues(project: project, tracker: tracker, sort: 'id:desc')
269
- if last_issues = last_issues_for_watch[watch]
270
- new_issues = []
271
- issues.each do |issue|
272
- found = last_issues.find do |last_issue|
273
- last_issue.id == issue.id
274
- end
275
-
276
- if found
277
- break
278
- else
279
- new_issues << issue
280
- end
281
- end
282
-
283
- new_issues.each do |new_issue|
284
- assignees = watch['assignees']
285
- assignee = nil
286
- if !assignees.empty? && !new_issue.assigned_to
287
- assignees -= absent_users
288
- assignee = assignees[watch['assignee_index'] % assignees.size]
289
- watch['assignee_index'] += 1
290
-
291
- assignee = find_user_by_id(assignee)
292
- end
293
-
294
- if assignee
295
- redmine.update_issue(new_issue, assigned_to_id: assignee['redmine_id'])
296
- end
297
-
298
- msg = <<-EOC
299
- New Issue of #{tracker.name} in #{project.name} project
300
- -> #{new_issue.subject}
301
- EOC
302
-
303
- if assignee
304
- msg += <<-EOC
305
- -> Assigned to @#{assignee['chat_name']}
306
- EOC
307
- end
308
-
309
- msg += <<-EOC
310
- -> #{redmine.url_for_issue(new_issue)}
311
- EOC
312
-
313
- Message.new(
314
- watch.symbolize_keys.merge(robot: robot)
315
- ).reply(msg)
316
- end
317
- end
318
-
319
- last_issues_for_watch[watch] = issues
320
- end
321
- end
322
- end
323
-
324
- thread.abort_on_exception = true
325
- end
326
242
  end
327
243
  end
328
244
  end
@@ -68,6 +68,36 @@ module Ruboty
68
68
  OpenStruct.new(
69
69
  JSON.parse(post('/issues.json', req).body)['issue']
70
70
  )
71
+ # {
72
+ # "issue": {
73
+ # "id": 1,
74
+ # "project": {
75
+ # "id": 1,
76
+ # "name": "..."
77
+ # },
78
+ # "tracker": {
79
+ # "id": 1,
80
+ # "name": "..."
81
+ # },
82
+ # "status": {
83
+ # "id": 1,
84
+ # "name": "new"
85
+ # },
86
+ # "priority": {
87
+ # "id": 4,
88
+ # "name": "通常"
89
+ # },
90
+ # "author": {
91
+ # "id": 1,
92
+ # "name": "Arai Ryota"
93
+ # },
94
+ # "subject": "This is test",
95
+ # "start_date": "2017-02-01",
96
+ # "done_ratio": 0,
97
+ # "created_on": "2017-02-01T11:10:35Z",
98
+ # "updated_on": "2017-02-01T11:10:35Z"
99
+ # }
100
+ # }
71
101
  end
72
102
 
73
103
  def update_issue(issue, opts)
@@ -1,5 +1,5 @@
1
1
  module Ruboty
2
2
  module Redmine
3
- VERSION = "0.1.7"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
@@ -20,8 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.add_dependency "ruboty"
21
21
  spec.add_dependency "faraday"
22
22
  spec.add_dependency "activesupport"
23
+ spec.add_dependency "slack-notifier"
23
24
 
24
25
  spec.add_development_dependency "pry-byebug"
26
+ spec.add_development_dependency "ruboty-slack_rtm"
25
27
  spec.add_development_dependency "bundler", "~> 1.7"
26
28
  spec.add_development_dependency "rake", "~> 10.0"
27
29
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruboty-redmine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryota Arai
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-22 00:00:00.000000000 Z
11
+ date: 2017-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruboty
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: slack-notifier
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: pry-byebug
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - ">="
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ruboty-slack_rtm
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: bundler
71
99
  requirement: !ruby/object:Gem::Requirement