bender-bot 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Readme.md +1 -1
- data/VERSION +1 -1
- data/lib/bender/bot.rb +164 -12
- data/lib/bender/main.rb +85 -1
- metadata +44 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c561e6f44af4f398b20ae38a1c8b506a3318a4b
|
4
|
+
data.tar.gz: 736e2de7ab537a0d070b9d65fc06f3a1831c1bc6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc3a5c412677a9dbe0d3bdf61c7fecacff394636f7ecf9ca3c451964905c163ae2a7377a13a66215c8700dd4687a378c893c0b2e069826dc3d609c49c2dc9e96
|
7
|
+
data.tar.gz: 13229d4431b57107fbf104d598ff36d645db1d96086e9e5f9930eafd48eb6361f27ed1e18d938775fa2668949631ee35c367e2c7e4aabd7c50bb5fec9f489bd6
|
data/Readme.md
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/lib/bender/bot.rb
CHANGED
@@ -2,33 +2,185 @@ require 'thread'
|
|
2
2
|
|
3
3
|
require 'robut'
|
4
4
|
require 'robut/storage/yaml_store'
|
5
|
+
require 'fuzzystringmatch'
|
6
|
+
require 'queryparams'
|
5
7
|
|
6
8
|
Bot = Robut # alias
|
7
9
|
|
8
10
|
|
9
11
|
|
10
12
|
module Bot
|
11
|
-
def self.run!
|
12
|
-
|
13
|
-
Bot::
|
13
|
+
def self.run! options
|
14
|
+
BenderBot.class_variable_set :@@options, options
|
15
|
+
Bot::Plugin.plugins = [ BenderBot ]
|
16
|
+
conn = Bot::Connection.new
|
17
|
+
conn.store['users'] ||= {}
|
18
|
+
Bot::Web.set :connection, conn.connect
|
19
|
+
return conn
|
14
20
|
end
|
15
21
|
end
|
16
22
|
|
17
23
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
24
|
+
class BenderBot
|
25
|
+
include Bot::Plugin
|
26
|
+
|
27
|
+
JARO = FuzzyStringMatch::JaroWinkler.create :native
|
28
|
+
|
29
|
+
SHOW_FIELDS = %w[
|
30
|
+
summary
|
31
|
+
description
|
32
|
+
priority
|
33
|
+
status
|
34
|
+
created
|
35
|
+
updated
|
36
|
+
]
|
37
|
+
|
38
|
+
|
39
|
+
def handle time, sender, message
|
40
|
+
case message
|
41
|
+
|
42
|
+
|
43
|
+
when /^\s*\?opts\s*$/
|
44
|
+
reply options.inspect
|
45
|
+
|
46
|
+
when /^\s*\?whoami\s*$/
|
47
|
+
u = user_where name: sender
|
48
|
+
reply '%s: %s (%s)' % [ u[:nick], u[:name], u[:email] ]
|
49
|
+
|
50
|
+
when /^\s*\?lookup\s+(.+)\s*$/
|
51
|
+
u = user_where(name: $1) || user_where(nick: $1)
|
52
|
+
reply '%s: %s (%s)' % [ u[:nick], u[:name], u[:email] ]
|
53
|
+
|
54
|
+
when /^\s*\?incident\s*$/
|
55
|
+
reply [
|
56
|
+
'?icident - This help text',
|
57
|
+
'?incidents - List open incidents',
|
58
|
+
'?incident NUM - Show incident',
|
59
|
+
'?incident NUM FIELD - Show incident field',
|
60
|
+
'?incident SUMMARY - File a new incident'
|
61
|
+
].join("\n")
|
62
|
+
|
63
|
+
when /^\s*\?incidents\s*$/
|
64
|
+
refresh_incidents
|
22
65
|
|
66
|
+
is = store['incidents'].map do |i|
|
67
|
+
'%s: %s' % [ i['num'], i['fields']['summary'] ]
|
68
|
+
end.join("\n")
|
23
69
|
|
24
|
-
|
25
|
-
|
26
|
-
|
70
|
+
reply is
|
71
|
+
|
72
|
+
when /^\s*\?incident\s+(\d+)\s*$/
|
73
|
+
refresh_incidents
|
74
|
+
incident = store['incidents'].select { |i| i['num'] == $1 }.first
|
75
|
+
|
76
|
+
fields = SHOW_FIELDS - %w[ summary ]
|
77
|
+
|
78
|
+
i = fields.map do |f|
|
79
|
+
val = incident['fields'][f]
|
80
|
+
if val
|
81
|
+
val = val.is_a?(Hash) ? val['name'] : val
|
82
|
+
'%s: %s' % [ f, val ]
|
27
83
|
end
|
84
|
+
end.compact
|
85
|
+
|
86
|
+
reply "%s: %s\n%s" % [
|
87
|
+
incident['key'],
|
88
|
+
incident['fields']['summary'],
|
89
|
+
i.join("\n")
|
90
|
+
]
|
91
|
+
|
92
|
+
when /^\s*\?incident\s+(\d+)\s+(.*?)\s*$/
|
93
|
+
refresh_incidents
|
94
|
+
incident = store['incidents'].select { |i| i['num'] == $1 }.first
|
95
|
+
val = incident['fields'][$2]
|
96
|
+
val = val.is_a?(Hash) ? val['name'] : val
|
97
|
+
reply val
|
98
|
+
|
99
|
+
when /^\s*\?incident\s+(.*?)\s*$/
|
100
|
+
user = user_where name: sender
|
101
|
+
data = {
|
102
|
+
fields: {
|
103
|
+
project: { key: options.jira_project },
|
104
|
+
issuetype: { name: options.jira_type },
|
105
|
+
reporter: { name: user[:nick] },
|
106
|
+
summary: $1
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
reply file_incident(data)
|
111
|
+
end
|
112
|
+
|
113
|
+
return true
|
114
|
+
end
|
115
|
+
|
28
116
|
|
29
|
-
return true
|
30
|
-
end
|
31
117
|
|
118
|
+
private
|
119
|
+
|
120
|
+
def options ; @@options end
|
121
|
+
|
122
|
+
def user_where fields, threshold=0.8
|
123
|
+
field, value = fields.to_a.shift
|
124
|
+
suggested_user = store['users'].values.sort_by do |u|
|
125
|
+
compare value, u[field]
|
126
|
+
end.last
|
127
|
+
|
128
|
+
distance = compare value, suggested_user[field]
|
129
|
+
return distance < threshold ? nil : suggested_user
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
def compare name1, name2
|
134
|
+
n1 = name1.gsub /\W/, ''
|
135
|
+
n2 = name2.gsub /\W/, ''
|
136
|
+
d1 = JARO.getDistance n1.downcase, n2.downcase
|
137
|
+
d2 = JARO.getDistance n1, n2
|
138
|
+
return d1 + d2 / 2.0
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
|
143
|
+
def refresh_incidents
|
144
|
+
req_path = '/rest/api/2/search'
|
145
|
+
req_params = QueryParams.encode \
|
146
|
+
jql: "project = #{options.jira_project} AND resolution = Unresolved ORDER BY created ASC, priority DESC",
|
147
|
+
fields: SHOW_FIELDS.join(','),
|
148
|
+
startAt: 0,
|
149
|
+
maxResults: 1_000_000
|
150
|
+
|
151
|
+
uri = URI(options.jira_site + req_path + '?' + req_params)
|
152
|
+
http = Net::HTTP.new uri.hostname, uri.port
|
153
|
+
|
154
|
+
req = Net::HTTP::Get.new uri
|
155
|
+
req.basic_auth options.jira_user, options.jira_pass
|
156
|
+
req['Content-Type'] = 'application/json'
|
157
|
+
req['Accept'] = 'application/json'
|
158
|
+
|
159
|
+
resp = http.request req
|
160
|
+
issues = JSON.parse(resp.body)['issues']
|
161
|
+
|
162
|
+
store['incidents'] = issues.map! do |i|
|
163
|
+
i['num'] = i['key'].split('-', 2).last ; i
|
32
164
|
end
|
33
165
|
end
|
166
|
+
|
167
|
+
|
168
|
+
def file_incident data
|
169
|
+
req_path = '/rest/api/2/issue'
|
170
|
+
uri = URI(options.jira_site + req_path)
|
171
|
+
http = Net::HTTP.new uri.hostname, uri.port
|
172
|
+
|
173
|
+
req = Net::HTTP::Post.new uri
|
174
|
+
req.basic_auth options.jira_user, options.jira_pass
|
175
|
+
req['Content-Type'] = 'application/json'
|
176
|
+
req['Accept'] = 'application/json'
|
177
|
+
req.body = data.to_json
|
178
|
+
|
179
|
+
resp = http.request req
|
180
|
+
issue = JSON.parse(resp.body)
|
181
|
+
|
182
|
+
return options.jira_site + '/browse/' + issue['key']
|
183
|
+
end
|
184
|
+
|
185
|
+
|
34
186
|
end
|
data/lib/bender/main.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'logger'
|
2
2
|
|
3
3
|
require 'tilt/erb'
|
4
|
+
require 'queryparams'
|
4
5
|
|
5
6
|
require_relative 'metadata'
|
6
7
|
require_relative 'mjolnir'
|
@@ -70,8 +71,53 @@ module Bender
|
|
70
71
|
aliases: %w[ -d ],
|
71
72
|
desc: 'Set path to application database',
|
72
73
|
required: true
|
74
|
+
option :rooms, \
|
75
|
+
type: :string,
|
76
|
+
aliases: %w[ -r ],
|
77
|
+
desc: 'Set HipChat rooms (comma-separated)',
|
78
|
+
required: true
|
79
|
+
option :jira_user, \
|
80
|
+
type: :string,
|
81
|
+
aliases: %w[ -U ],
|
82
|
+
desc: 'Set JIRA username',
|
83
|
+
required: true
|
84
|
+
option :jira_pass, \
|
85
|
+
type: :string,
|
86
|
+
aliases: %w[ -P ],
|
87
|
+
desc: 'Set JIRA password',
|
88
|
+
required: true
|
89
|
+
option :jira_site, \
|
90
|
+
type: :string,
|
91
|
+
aliases: %w[ -S ],
|
92
|
+
desc: 'Set JIRA site',
|
93
|
+
required: true
|
94
|
+
option :jira_project, \
|
95
|
+
type: :string,
|
96
|
+
aliases: %w[ -J ],
|
97
|
+
desc: 'Set JIRA project',
|
98
|
+
required: true
|
99
|
+
option :jira_type, \
|
100
|
+
type: :string,
|
101
|
+
aliases: %w[ -T ],
|
102
|
+
desc: 'Set JIRA issue type',
|
103
|
+
required: true
|
104
|
+
option :refresh, \
|
105
|
+
type: :numeric,
|
106
|
+
aliases: %w[ -R ],
|
107
|
+
desc: 'Set JIRA refresh rate',
|
108
|
+
default: 300
|
73
109
|
include_common_options
|
74
110
|
def start
|
111
|
+
bot = start_bot
|
112
|
+
refresh_users bot
|
113
|
+
serve_web bot
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def start_bot
|
75
121
|
Bot::Connection.configure do |config|
|
76
122
|
config.jid = options.jid
|
77
123
|
config.password = options.password
|
@@ -85,9 +131,46 @@ module Bender
|
|
85
131
|
config.logger = log
|
86
132
|
end
|
87
133
|
|
88
|
-
Bot.run!
|
134
|
+
Bot.run! options
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
def refresh_users bot
|
139
|
+
req_path = '/rest/api/2/user/assignable/search'
|
140
|
+
req_params = QueryParams.encode \
|
141
|
+
project: options.jira_project,
|
142
|
+
startAt: 0,
|
143
|
+
maxResults: 1_000_000
|
144
|
+
|
145
|
+
uri = URI(options.jira_site + req_path + '?' + req_params)
|
146
|
+
http = Net::HTTP.new uri.hostname, uri.port
|
147
|
+
|
148
|
+
req = Net::HTTP::Get.new uri
|
149
|
+
req.basic_auth options.jira_user, options.jira_pass
|
150
|
+
req['Content-Type'] = 'application/json'
|
151
|
+
req['Accept'] = 'application/json'
|
152
|
+
|
153
|
+
Thread.new do
|
154
|
+
loop do
|
155
|
+
resp = http.request req
|
156
|
+
|
157
|
+
users = JSON.parse(resp.body).inject({}) do |h, user|
|
158
|
+
h[user['key']] = {
|
159
|
+
nick: user['key'],
|
160
|
+
name: user['displayName'],
|
161
|
+
email: user['emailAddress']
|
162
|
+
} ; h
|
163
|
+
end
|
164
|
+
|
165
|
+
bot.store['users'] = users
|
166
|
+
|
167
|
+
sleep options.refresh
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
89
171
|
|
90
172
|
|
173
|
+
def serve_web bot
|
91
174
|
Web.set :environment, options.environment
|
92
175
|
Web.set :port, options.port
|
93
176
|
Web.set :bind, options.bind
|
@@ -100,6 +183,7 @@ module Bender
|
|
100
183
|
Web.set :logging, ::Logger::DEBUG
|
101
184
|
end
|
102
185
|
|
186
|
+
Web.set :bot, bot
|
103
187
|
Web.run!
|
104
188
|
end
|
105
189
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bender-bot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sean Clemmer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-04-
|
11
|
+
date: 2015-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -66,6 +66,48 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 0.5.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: hipchat
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: queryparams
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.0.3
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.0.3
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: fuzzy-string-match
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.9.7
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.9.7
|
69
111
|
- !ruby/object:Gem::Dependency
|
70
112
|
name: eventmachine
|
71
113
|
requirement: !ruby/object:Gem::Requirement
|