ruboty-redmine 0.1.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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