bender-bot 0.0.1 → 0.0.2
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/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
|