cksh_commander 0.2.1 → 0.2.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 +81 -3
- data/commands/jira/Gemfile +4 -0
- data/commands/jira/command.rb +84 -0
- data/commands/lunch/Gemfile +4 -0
- data/commands/lunch/command.rb +96 -0
- data/commands/lunch/in.txt +0 -0
- data/lib/cksh_commander.rb +1 -5
- data/lib/cksh_commander/command.rb +28 -1
- data/lib/cksh_commander/runner.rb +4 -2
- data/lib/cksh_commander/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f98c333fb1994d93150486351c05fac07578f1b1
|
4
|
+
data.tar.gz: 587802b3cb82c7bf0da152b10d5a25d836f7d1b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63295c5c7b1c4107287cac3ffb856f23828bef9be29fb45f6e4369091e192bad95bf15bb04efc4e5b38da4cce7be3ca1b3040d9b8f5ada096147da693f6c871a
|
7
|
+
data.tar.gz: bbbfe5fc04d24897757a5ef04f382e837fdbb631481d95d181621195f0cad60192471254b0318178c6677e701ed4073dd17ed11ae873eea453fad90fea43b34d
|
data/README.md
CHANGED
@@ -10,6 +10,15 @@ arguments. Check out the
|
|
10
10
|
that comes bootstrapped with everything you need to process Slack slash
|
11
11
|
commands using this gem.
|
12
12
|
|
13
|
+
- [Installation](#installation)
|
14
|
+
- [Usage](#usage)
|
15
|
+
- [Getting Started](#getting-started)
|
16
|
+
- [Command API](#command-api)
|
17
|
+
- [Running a Command](#running-a-command)
|
18
|
+
- [Examples](#examples)
|
19
|
+
- [Development](#development)
|
20
|
+
- [Contributing](#contributing)
|
21
|
+
|
13
22
|
### Installation
|
14
23
|
|
15
24
|
You can install `cksh_commander` via RubyGems. In your Gemfile:
|
@@ -26,6 +35,8 @@ gem install cksh_commander
|
|
26
35
|
|
27
36
|
### Usage
|
28
37
|
|
38
|
+
#### Getting Started
|
39
|
+
|
29
40
|
Defining a custom command is easy. First, create a `commands/` directory and tell
|
30
41
|
`CKSHCommander` where it can find your commands. Let's say we've created a `commands/`
|
31
42
|
directory at the root of our project. Our configuration is as follows:
|
@@ -93,11 +104,28 @@ module Example
|
|
93
104
|
end
|
94
105
|
```
|
95
106
|
|
96
|
-
|
107
|
+
#### Command API
|
108
|
+
|
109
|
+
We `set` our slash command authentication token at the class level, and define
|
97
110
|
methods for processing subcommands. Attachments (which take the form of a hash)
|
98
111
|
can be added using `add_response_attachment(attachment)`. You can also set the
|
99
112
|
response to `'in_channel'` at the method level with `respond_in_channel!`.
|
100
113
|
|
114
|
+
We can access the Slack payload data via the `data` reader.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
data.token #=> "gIkuvaNzQIHg97ATvDxqgjtO"
|
118
|
+
data.team_id #=> "T0001"
|
119
|
+
data.team_domain #=> "example"
|
120
|
+
data.channel_id #=> "C2147483705"
|
121
|
+
data.channel_name #=> "test"
|
122
|
+
data.user_id #=> "U2147483697"
|
123
|
+
data.user_name #=> "Randy"
|
124
|
+
data.command #=> "/test"
|
125
|
+
data.text #=> "subcommand"
|
126
|
+
data.response_url #=> "https://hooks.slack.com/commands/1234/5678"
|
127
|
+
```
|
128
|
+
|
101
129
|
Similar to [Thor](http://whatisthor.com/), we are able to document our
|
102
130
|
command's API with the class-level `desc` method—as is shown in the example
|
103
131
|
above. CKSHCommander provides a `help` subcommand out of the box, and this will
|
@@ -108,10 +136,47 @@ echo the documentation back to the Slack user.
|
|
108
136
|
/example test1 [TEXT] # Subcommand 1 description.
|
109
137
|
/example [TEXT] # Root command description.
|
110
138
|
```
|
139
|
+
Use `authorize` to whitelist the user IDs of Slack users whom you've authorized
|
140
|
+
to perform a subcommand. An unauthorized user who tries to use this subcommand will
|
141
|
+
receive the response, "You are unauthorized to use this subcommand!" You can
|
142
|
+
find the IDs of Slack users on your team using
|
143
|
+
[Slack's REST API](https://slack.com/api/users.list?token=YOURTOKEN). Note that
|
144
|
+
authorization is not performed unless `authorize` is used explicitly.
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
...
|
148
|
+
desc "privatecmd", "A private subcommand."
|
149
|
+
def privatecmd
|
150
|
+
authorize(%w[U2147483697])
|
151
|
+
set_response_text("You are authorized!")
|
152
|
+
end
|
153
|
+
...
|
154
|
+
```
|
155
|
+
|
156
|
+
If you need to debug a subcommand and forward a caught exception to your Slack
|
157
|
+
client, you can use `debug!` at the top of your subcommand's method body.
|
158
|
+
Otherwise, exceptions at the subcommand level will result in the `set` error
|
159
|
+
message text being returned to the client. Using `debug!` like this allows you
|
160
|
+
to isolate testing to a single subcommand without disrupting all usage of the
|
161
|
+
slash command.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
...
|
165
|
+
set error_message: "Hm. Something went wrong!"
|
166
|
+
|
167
|
+
def subcommand
|
168
|
+
debug!
|
169
|
+
undefined_method #=> NameError
|
170
|
+
set_response_text("Never evaluated...")
|
171
|
+
end
|
172
|
+
...
|
173
|
+
```
|
174
|
+
|
175
|
+
#### Running a Command
|
111
176
|
|
112
177
|
To run a command, we use `CKSHCommander::Runner`. In the example below, we've
|
113
|
-
created a simple Sinatra app
|
114
|
-
slash command payload.
|
178
|
+
created a [simple Sinatra app](https://github.com/openarcllc/cksh_commander_api)
|
179
|
+
to illustrate its usage with the standard Slack slash command payload.
|
115
180
|
|
116
181
|
```ruby
|
117
182
|
# app.rb
|
@@ -133,6 +198,19 @@ post "/" do
|
|
133
198
|
end
|
134
199
|
```
|
135
200
|
|
201
|
+
### Examples
|
202
|
+
|
203
|
+
Check out (and/or use) [example commands provided by the
|
204
|
+
community](https://github.com/openarcllc/cksh_commander/tree/master/commands).
|
205
|
+
Want to contribute a command? Great! Add your command to the `commands/` directory
|
206
|
+
and open a pull request! The implementations below provide convenient reference
|
207
|
+
points.
|
208
|
+
|
209
|
+
| command | description |
|
210
|
+
| --------|--------------- |
|
211
|
+
| [jira](https://github.com/openarcllc/cksh_commander/tree/master/commands/jira) | Display JIRA issue details in a Slack channel or message. |
|
212
|
+
| [lunch](https://github.com/openarcllc/cksh_commander/tree/master/commands/lunch) | State or revoke your intention to attend the weekly lunch gathering. |
|
213
|
+
|
136
214
|
### Development
|
137
215
|
|
138
216
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "cksh_commander"
|
2
|
+
require "jira"
|
3
|
+
|
4
|
+
module Jira
|
5
|
+
class Command < CKSHCommander::Command
|
6
|
+
set token: "YOUR_SLASH_COMMAND_TOKEN"
|
7
|
+
|
8
|
+
# Location of your JIRA instance
|
9
|
+
JIRABASE = "http://localhost:3000/jira"
|
10
|
+
|
11
|
+
desc "issues [PROJECT] [STATUS]", "Get project issues."
|
12
|
+
def issues(project, status = nil)
|
13
|
+
content = "Issues for project: *#{project.upcase}*\n\n"
|
14
|
+
|
15
|
+
begin
|
16
|
+
issues = get_issues(project, status)
|
17
|
+
if issues.any?
|
18
|
+
issueoutput = issues.map { |i| issue_output(i) }.join("\n")
|
19
|
+
respond_in_channel!
|
20
|
+
add_response_attachment({
|
21
|
+
text: issueoutput, mrkdwn_in: ['text'], color: 'good'
|
22
|
+
})
|
23
|
+
else
|
24
|
+
content += "No matching issues found..."
|
25
|
+
end
|
26
|
+
rescue
|
27
|
+
content = "Encountered an error..."
|
28
|
+
end
|
29
|
+
|
30
|
+
set_response_text(content)
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "[TICKET]", "Get ticket details."
|
34
|
+
def ___(issueid)
|
35
|
+
begin
|
36
|
+
issue = client.Issue.find(issueid)
|
37
|
+
set_response_text(issue_output(issue))
|
38
|
+
respond_in_channel!
|
39
|
+
rescue
|
40
|
+
set_response_text("Cannot find issue!")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def client
|
47
|
+
@jira ||= JIRA::Client.new({
|
48
|
+
username: "YOUR_JIRA_SYSTEM_USERNAME",
|
49
|
+
password: "YOUR_JIRA_SYSTEM_PASSWORD",
|
50
|
+
site: JIRABASE,
|
51
|
+
auth_type: :basic,
|
52
|
+
context_path: ""
|
53
|
+
})
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_issues(project, status)
|
57
|
+
query = "project = #{project}"
|
58
|
+
query += " AND status = \"#{status}\"" if status
|
59
|
+
|
60
|
+
client.Issue.jql(query, {
|
61
|
+
startAt: 0,
|
62
|
+
max: 50,
|
63
|
+
fields: %w[summary assignee status issuetype key]
|
64
|
+
})
|
65
|
+
end
|
66
|
+
|
67
|
+
def issue_output(issue)
|
68
|
+
output = "<#{JIRABASE}/browse/#{issue.key}|*#{issue.key}*: #{issue.summary}>\n"
|
69
|
+
output += "*Type*: #{issue.fields['issuetype']['name']}\n"
|
70
|
+
output += "*Assignee*: #{assignee_output(issue.fields['assignee'])}\n"
|
71
|
+
output += "*Status*: #{issue.fields['status']['name']}\n"
|
72
|
+
end
|
73
|
+
|
74
|
+
def assignee_output(assignee)
|
75
|
+
return "Not Assigned" unless assignee
|
76
|
+
"#{assignee["displayName"]} " +
|
77
|
+
"<#{user_email(assignee["emailAddress"])}>"
|
78
|
+
end
|
79
|
+
|
80
|
+
def user_email(address)
|
81
|
+
"<mailto:#{address}|#{address}>"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "cksh_commander"
|
2
|
+
require "httparty"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Lunch
|
6
|
+
class Command < CKSHCommander::Command
|
7
|
+
set token: "YOUR_SLASH_COMMAND_TOKEN"
|
8
|
+
|
9
|
+
# Keep track of who is in for lunch.
|
10
|
+
INFILE = File.expand_path("../in.txt", __FILE__)
|
11
|
+
|
12
|
+
desc "in", "State intention to attend the upcoming lunch."
|
13
|
+
def in
|
14
|
+
if user_in?
|
15
|
+
set_response_text("You're already in!")
|
16
|
+
else
|
17
|
+
add_user
|
18
|
+
set_response_text("You're in!")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "out", "Revoke intention to attend the upcoming lunch."
|
23
|
+
def out
|
24
|
+
if !user_in?
|
25
|
+
set_response_text("You aren't in, so you can't be out!")
|
26
|
+
else
|
27
|
+
remove_user
|
28
|
+
set_response_text("You're out!")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "who", "See who is attending the upcoming lunch."
|
33
|
+
def who
|
34
|
+
if attendees.any?
|
35
|
+
content = "Those IN for lunch: "
|
36
|
+
attendees.each_with_index do |a, i|
|
37
|
+
content += username_from_slug(a)
|
38
|
+
content += ", " unless i + 1 == attendees.count
|
39
|
+
end
|
40
|
+
else
|
41
|
+
content = "No one has committed to lunch..."
|
42
|
+
end
|
43
|
+
|
44
|
+
set_response_text(content)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Allow a user to send out a lunch reminder; this requires Slack
|
48
|
+
# incoming webhook integration: https://api.slack.com/incoming-webhooks
|
49
|
+
def remind
|
50
|
+
payload = { text: "Lunch alert! Food in ~10 minutes!" }
|
51
|
+
|
52
|
+
attendees.each do |a|
|
53
|
+
# NOTE: Skip reminding the reminder...
|
54
|
+
# next if a == userslug
|
55
|
+
|
56
|
+
HTTParty.post(
|
57
|
+
"YOUR_SLACK_WEBHOOK_URL",
|
58
|
+
body: payload.merge({ channel: "@#{username_from_slug(a)}" }).to_json,
|
59
|
+
headers: { "Content-Type" => "application/json" })
|
60
|
+
end
|
61
|
+
|
62
|
+
set_response_text(attendees.any? ? "Reminders sent!" : "No one to remind...")
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def attendees
|
68
|
+
File.read(INFILE).split("\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_user(slug = nil)
|
72
|
+
slug ||= userslug
|
73
|
+
File.open(INFILE, 'a') { |f| f.write("#{slug}\n") }
|
74
|
+
end
|
75
|
+
|
76
|
+
def remove_user
|
77
|
+
attending = attendees.dup
|
78
|
+
File.open(INFILE, 'w')
|
79
|
+
attending.dup.each do |a|
|
80
|
+
add_user(a) if userslug != a && a.size > 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def user_in?
|
85
|
+
attendees.include?(userslug)
|
86
|
+
end
|
87
|
+
|
88
|
+
def userslug
|
89
|
+
"#{data.user_name}|#{data.user_id}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def username_from_slug(slug)
|
93
|
+
slug.split('|')[0]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
File without changes
|
data/lib/cksh_commander.rb
CHANGED
@@ -3,7 +3,7 @@ require "cksh_commander/response"
|
|
3
3
|
module CKSHCommander
|
4
4
|
class Command
|
5
5
|
class << self
|
6
|
-
attr_accessor :token
|
6
|
+
attr_accessor :token, :error_message
|
7
7
|
attr_reader :docs
|
8
8
|
|
9
9
|
def set(opts)
|
@@ -24,6 +24,8 @@ module CKSHCommander
|
|
24
24
|
|
25
25
|
def initialize(data = nil)
|
26
26
|
@data = data
|
27
|
+
@authorized = true
|
28
|
+
@debugging = false
|
27
29
|
@response = Response.new
|
28
30
|
end
|
29
31
|
|
@@ -83,10 +85,12 @@ module CKSHCommander
|
|
83
85
|
end
|
84
86
|
|
85
87
|
def set_response_text(text)
|
88
|
+
return unless @authorized
|
86
89
|
@response.text = text
|
87
90
|
end
|
88
91
|
|
89
92
|
def add_response_attachment(attachment)
|
93
|
+
return unless @authorized
|
90
94
|
unless attachment.is_a?(Hash)
|
91
95
|
raise ArgumentError, "Attachment must be a Hash"
|
92
96
|
end
|
@@ -95,9 +99,32 @@ module CKSHCommander
|
|
95
99
|
end
|
96
100
|
|
97
101
|
def respond_in_channel!
|
102
|
+
return unless @authorized
|
98
103
|
@response.type = 'in_channel'
|
99
104
|
end
|
100
105
|
|
106
|
+
# Authorize users to use a subcommand by
|
107
|
+
# whitelisting user IDs.
|
108
|
+
def authorize(ids)
|
109
|
+
@authorized = ids.any? { |id| @data.user_id == id }
|
110
|
+
unless @authorized
|
111
|
+
@response = Response.new("You are unauthorized to use this subcommand!")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def debug!
|
116
|
+
@debugging = true
|
117
|
+
end
|
118
|
+
|
119
|
+
def debugging?
|
120
|
+
@debugging
|
121
|
+
end
|
122
|
+
|
123
|
+
def error_message
|
124
|
+
self.class.error_message ||
|
125
|
+
"Something went awry..."
|
126
|
+
end
|
127
|
+
|
101
128
|
private
|
102
129
|
|
103
130
|
def subcommand_methods
|
@@ -12,8 +12,10 @@ module CKSHCommander
|
|
12
12
|
cmd = Kernel.const_get(name).new(data)
|
13
13
|
|
14
14
|
response = cmd.authenticated? ? cmd.run : Response.new("Invalid token!")
|
15
|
-
rescue
|
16
|
-
|
15
|
+
rescue => e
|
16
|
+
text = cmd && cmd.debugging? ? e.message :
|
17
|
+
cmd ? cmd.error_message : "Command not found..."
|
18
|
+
response = Response.new(text)
|
17
19
|
end
|
18
20
|
|
19
21
|
response
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cksh_commander
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Travis Loncar
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-12-
|
11
|
+
date: 2015-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -68,6 +68,11 @@ files:
|
|
68
68
|
- bin/console
|
69
69
|
- bin/setup
|
70
70
|
- cksh_commander.gemspec
|
71
|
+
- commands/jira/Gemfile
|
72
|
+
- commands/jira/command.rb
|
73
|
+
- commands/lunch/Gemfile
|
74
|
+
- commands/lunch/command.rb
|
75
|
+
- commands/lunch/in.txt
|
71
76
|
- lib/cksh_commander.rb
|
72
77
|
- lib/cksh_commander/command.rb
|
73
78
|
- lib/cksh_commander/data.rb
|