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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/Readme.md +1 -1
  3. data/VERSION +1 -1
  4. data/lib/bender/bot.rb +164 -12
  5. data/lib/bender/main.rb +85 -1
  6. metadata +44 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 96464ae31bbfe17baeab335880504aa1900a363a
4
- data.tar.gz: 6c140ca83b456be05cd33c7f7c24d27c28d00fe8
3
+ metadata.gz: 7c561e6f44af4f398b20ae38a1c8b506a3318a4b
4
+ data.tar.gz: 736e2de7ab537a0d070b9d65fc06f3a1831c1bc6
5
5
  SHA512:
6
- metadata.gz: 7e29c40bb49d93c64bcc04c9292f557ba021a51a222a65d9a5e44ab2006fdc6dc5f2aff5dd3db5b6588bb1c0115655987fea8da34685be5fd77a25ac98636fbc
7
- data.tar.gz: 1c5505dbe45432ace49b97196ca3f89a182622f19eb48b23f29fa72862891734f83f5948af7781e77691efbcd44e75b444f63f33f690e941804d12e8266fea50
6
+ metadata.gz: fc3a5c412677a9dbe0d3bdf61c7fecacff394636f7ecf9ca3c451964905c163ae2a7377a13a66215c8700dd4687a378c893c0b2e069826dc3d609c49c2dc9e96
7
+ data.tar.gz: 13229d4431b57107fbf104d598ff36d645db1d96086e9e5f9930eafd48eb6361f27ed1e18d938775fa2668949631ee35c367e2c7e4aabd7c50bb5fec9f489bd6
data/Readme.md CHANGED
@@ -1,3 +1,3 @@
1
- # Bender ![Version](https://img.shields.io/gem/v/bender.svg?style=flat-square)
1
+ # Bender ![Version](https://img.shields.io/gem/v/bender-bot.svg?style=flat-square)
2
2
 
3
3
  Yet another HipChat bot.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
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
- Bot::Plugin.plugins = [ Bot::Plugin::Bender ]
13
- Bot::Web.set :connection, Bot::Connection.new.connect
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
- module Bot
19
- module Plugin
20
- class Bender
21
- include Bot::Plugin
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
- def handle time, sender, message
25
- if message =~ /bender/
26
- reply 'What, %s?! (%s)' % [ sender, time ]
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.1
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-23 00:00:00.000000000 Z
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