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.
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