mos-eisley-lambda 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -29
- data/lib/handler.rb +27 -10
- data/lib/mos-eisley-lambda.rb +108 -84
- data/lib/s3po/blockkit.rb +26 -0
- data/lib/s3po/s3po.rb +4 -0
- data/lib/slack.rb +3 -3
- data/mos-eisley-lambda.gemspec +2 -2
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e38c9c1ecdcab343fc118677a1f7b515d4fe4ce19a0b4a056b50f515f81f87b
|
4
|
+
data.tar.gz: a0b90f7bfb4c67bf383ce059fab48a7ce93d0588bd0766603dc879ea14783789
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 31a6c405c9b10616063c4c7d99579d16bbfbd816ba1476640662a9123aa631d8d51886ee39a1f1007ff7fa1b2b7c46ecf10cbf24dd7f6b45629bbc102c47f43f
|
7
|
+
data.tar.gz: 3dca76f821c4d4be95544eccdc599be61eef6715cdf0f7083649b3ac8ac8cc934d138c7757a31d2d3461491070d6327f8444abc143028f8d438b14a3b3540606
|
data/README.md
CHANGED
@@ -4,13 +4,12 @@
|
|
4
4
|
|
5
5
|
“You will never find a more wretched hive of scum and villainy.” – Obi-Wan Kenobi
|
6
6
|
|
7
|
-
Episode 2 of the Ruby based [Slack app](https://api.slack.com/) framework, this time for [AWS Lambda](https://aws.amazon.com/lambda/). Pure
|
7
|
+
Episode 2 of the Ruby based [Slack app](https://api.slack.com/) framework, this time for [AWS Lambda](https://aws.amazon.com/lambda/). Pure Ruby, no external gem/library dependency.
|
8
8
|
|
9
9
|
## Setup
|
10
10
|
|
11
11
|
### AWS
|
12
12
|
|
13
|
-
1. Create an SQS queue for MosEisley
|
14
13
|
1. Create an IAM role for MosEisley Lambda function
|
15
14
|
1. Create a Lambda function for MosEisley
|
16
15
|
- You can install this gem using [Lambda Layer](#using-with-lambda-layers) or just copy the `lib` directory to your Lambda code.
|
@@ -20,10 +19,11 @@ Episode 2 of the Ruby based [Slack app](https://api.slack.com/) framework, this
|
|
20
19
|
|
21
20
|
Configure Lambda environment variable.
|
22
21
|
|
23
|
-
- `
|
24
|
-
- `
|
25
|
-
- `
|
26
|
-
- `MOSEISLEY_LOG_LEVEL
|
22
|
+
- `SLACK_CREDENTIALS_SSMPS_PATH`: hierarchy path to System Managers Parameter Store; e.g., `/slack/credentials/` would reference two parameters:
|
23
|
+
- `/slack/credetials/signing_secret`
|
24
|
+
- `/slack/credetials/bot_access_token`
|
25
|
+
- `MOSEISLEY_LOG_LEVEL`: _optional_, could be `DEBUG`, `INFO`, `WARN`, or `ERROR`
|
26
|
+
- `SLACK_LOG_CHANNEL_ID`: _optional_, if you want to use `ME::SlackWeb.post_log()`
|
27
27
|
|
28
28
|
Configure Lambda code in your `lambda_function.rb` file.
|
29
29
|
|
@@ -37,7 +37,7 @@ MosEisley::Handler.import
|
|
37
37
|
# MosEisley::Handler.import_from_path('./my-handlers')
|
38
38
|
|
39
39
|
def lambda_handler(event:, context:)
|
40
|
-
MosEisley::lambda_event(event)
|
40
|
+
MosEisley::lambda_event(event, context)
|
41
41
|
end
|
42
42
|
```
|
43
43
|
|
@@ -85,19 +85,6 @@ ME::Handler.add(:command, 'A Slack command') do |event, myself|
|
|
85
85
|
end
|
86
86
|
```
|
87
87
|
|
88
|
-
## Protocols
|
89
|
-
|
90
|
-
### SQS
|
91
|
-
|
92
|
-
- Attributes
|
93
|
-
- `source`: `slack`, `moseisley`, or other
|
94
|
-
- `endpoint`: if source is `slack`, which endpoint it arrived at
|
95
|
-
- `destination`: `slack` or `moseisley`
|
96
|
-
- `api`: if destination is `slack`
|
97
|
-
- Message (JSON)
|
98
|
-
- `params`: object, meant to be passed to Slack API
|
99
|
-
- `payload`: the data/payload itself
|
100
|
-
|
101
88
|
### Helpers
|
102
89
|
|
103
90
|
- `ME::S3PO` – collection of helpers to analyze/create Slack messages.
|
@@ -108,23 +95,22 @@ end
|
|
108
95
|
### Inbound
|
109
96
|
|
110
97
|
1. Slack event is sent to Mos Eisley Lambda function via API Gateway
|
111
|
-
1. Slack event is verified and
|
98
|
+
1. Slack event is verified and produces a parsed object
|
112
99
|
1. If it's a slash command, MosEisley::Handler.command_acks is referenced and immediate response is sent
|
113
|
-
1. The original Slack event JSON is sent to
|
100
|
+
1. The original Slack event JSON is sent to the function in a recursive fashion (this is to return the inital response ASAP)
|
114
101
|
|
115
102
|
### Event Processing
|
116
103
|
|
117
|
-
1.
|
104
|
+
1. Lambda function is invoked by itself with the original Slack event
|
118
105
|
1. Handlers are called and processed according to original endpoint the event was sent to; actions, commands, events, menus
|
119
|
-
1.
|
106
|
+
1. Send a Slack message as necessary and the Slack event cycle is complete
|
120
107
|
|
121
|
-
<!-- ###
|
108
|
+
<!-- ### Outbound, Messaging Only
|
122
109
|
|
123
|
-
|
110
|
+
Invoke the function from another app to send a Slack message
|
124
111
|
|
125
|
-
1. Create a Slack message packaged to be sent to the API and
|
126
|
-
1. Message
|
127
|
-
1. Message is sent to Slack API -->
|
112
|
+
1. Create a Slack message packaged to be sent to the API and invoke the function
|
113
|
+
1. Message is received, then sent to Slack API according to payload-->
|
128
114
|
|
129
115
|
## Using with Lambda Layers
|
130
116
|
|
data/lib/handler.rb
CHANGED
@@ -19,17 +19,26 @@ module MosEisley
|
|
19
19
|
end
|
20
20
|
|
21
21
|
# Call as often as necessary to add handlers with blocks; each call creates a MosEisley::Handler object
|
22
|
-
# @param type [Symbol] :action | :command | :event | :menu
|
23
|
-
# @param name [String]
|
22
|
+
# @param type [Symbol] :action | :command_response | :command | :event | :menu
|
23
|
+
# @param name [String] required for type = :command_response, otherwise optional
|
24
24
|
def self.add(type, name = nil, &block)
|
25
|
+
if type == :command_response && name.nil?
|
26
|
+
raise ArgumentError.new('Name required for :command_response.')
|
27
|
+
end
|
25
28
|
@handlers ||= {
|
26
29
|
action: [],
|
30
|
+
command_response: {},
|
27
31
|
command: [],
|
28
32
|
event: [],
|
29
33
|
menu: [],
|
30
34
|
}
|
31
|
-
|
32
|
-
|
35
|
+
h = MosEisley::Handler.new(type, name, &block)
|
36
|
+
if type == :command_response
|
37
|
+
@handlers[type][name] = h
|
38
|
+
else
|
39
|
+
@handlers[type] << h
|
40
|
+
end
|
41
|
+
MosEisley.logger.debug("Added #{type} handler: #{h}")
|
33
42
|
end
|
34
43
|
|
35
44
|
# Example: {'/command' => {response_type: 'ephemeral', text: nil}}
|
@@ -48,14 +57,22 @@ module MosEisley
|
|
48
57
|
def self.run(type, event)
|
49
58
|
logger = MosEisley.logger
|
50
59
|
response = nil
|
51
|
-
|
52
|
-
|
53
|
-
if h
|
54
|
-
|
55
|
-
|
60
|
+
if type == :command_response
|
61
|
+
h = @handlers[type][event[:command]]
|
62
|
+
if h
|
63
|
+
response = h.run(event)
|
64
|
+
logger.info("Done running #{type} handlers.")
|
65
|
+
end
|
66
|
+
else
|
67
|
+
@handlers[type].each do |h|
|
68
|
+
response = h.run(event)
|
69
|
+
if h.stopped?
|
70
|
+
logger.debug('Handler stop was requested.')
|
71
|
+
break
|
72
|
+
end
|
56
73
|
end
|
74
|
+
logger.info("Done running #{type} handlers.")
|
57
75
|
end
|
58
|
-
logger.info("Done running #{type} handlers.")
|
59
76
|
response
|
60
77
|
end
|
61
78
|
|
data/lib/mos-eisley-lambda.rb
CHANGED
@@ -2,52 +2,88 @@ require_relative './logger'
|
|
2
2
|
require_relative './slack'
|
3
3
|
require_relative './s3po/s3po'
|
4
4
|
require_relative './handler'
|
5
|
-
require 'aws-sdk-
|
5
|
+
require 'aws-sdk-lambda'
|
6
|
+
require 'aws-sdk-ssm'
|
6
7
|
require 'base64'
|
7
8
|
require 'json'
|
8
9
|
|
9
10
|
ME = MosEisley
|
10
|
-
SQS = Aws::SQS::Client.new
|
11
11
|
|
12
12
|
module MosEisley
|
13
|
-
def self.
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def self.config
|
14
|
+
@config ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.lambda_event(event, context)
|
18
|
+
raise 'Pre-flight check failed!' unless preflightcheck
|
19
|
+
case
|
20
|
+
when event['routeKey']
|
21
|
+
# Inbound Slack event (via API GW)
|
17
22
|
MosEisley.logger.info('API GW event')
|
18
|
-
return apigw_event(event)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
MosEisley.logger.
|
23
|
-
return
|
23
|
+
return apigw_event(event, context)
|
24
|
+
when event.dig('Records',0,'eventSource') == 'MosEisley:Slack_event'
|
25
|
+
# Internal event (via invoke)
|
26
|
+
MosEisley.logger.info('Invoke event')
|
27
|
+
MosEisley.logger.debug("#{event}")
|
28
|
+
return invoke_event(event)
|
29
|
+
else
|
30
|
+
# Unknown event
|
31
|
+
MosEisley.logger.info('Unknown event')
|
32
|
+
return unknown_event(event)
|
24
33
|
end
|
25
34
|
end
|
26
35
|
|
27
36
|
def self.preflightcheck
|
28
|
-
|
29
|
-
|
30
|
-
|
37
|
+
if config[:timestamp]
|
38
|
+
MosEisley.logger.debug("Confing already loaded at: #{config[:timestamp]}")
|
39
|
+
return true
|
31
40
|
end
|
32
41
|
env_required = [
|
33
|
-
'
|
34
|
-
'SLACK_SIGNING_SECRET',
|
35
|
-
'SLACK_BOT_ACCESS_TOKEN',
|
42
|
+
'SLACK_CREDENTIALS_SSMPS_PATH',
|
36
43
|
]
|
37
44
|
env_optional = [
|
38
45
|
'MOSEISLEY_LOG_LEVEL',
|
46
|
+
'SLACK_LOG_CHANNEL_ID',
|
39
47
|
]
|
40
|
-
|
41
|
-
|
42
|
-
|
48
|
+
config_required = [
|
49
|
+
:signing_secret,
|
50
|
+
:bot_access_token,
|
51
|
+
]
|
52
|
+
l = ENV['MOSEISLEY_LOG_LEVEL']
|
53
|
+
if String === l && ['DEBUG', 'INFO', 'WARN', 'ERROR'].include?(l.upcase)
|
54
|
+
MosEisley.logger.level = eval("Logger::#{l.upcase}")
|
55
|
+
end
|
56
|
+
env_required.each do |v|
|
57
|
+
if ENV[v].nil?
|
58
|
+
MosEisley.logger.error("Missing environment variable: #{v}")
|
43
59
|
return false
|
44
60
|
end
|
61
|
+
case v
|
62
|
+
when 'SLACK_CREDENTIALS_SSMPS_PATH'
|
63
|
+
ssm = Aws::SSM::Client.new
|
64
|
+
rparams = {
|
65
|
+
path: ENV['SLACK_CREDENTIALS_SSMPS_PATH'],
|
66
|
+
with_decryption: true,
|
67
|
+
}
|
68
|
+
ssm.get_parameters_by_path(rparams).parameters.each do |prm|
|
69
|
+
k = prm[:name].split('/').last.to_sym
|
70
|
+
config[k] = prm[:value]
|
71
|
+
config_required.delete(k)
|
72
|
+
end
|
73
|
+
end
|
45
74
|
end
|
75
|
+
unless config_required.empty?
|
76
|
+
t = "Missing config values: #{config_required.join(', ')}"
|
77
|
+
MosEisley.logger.error(t)
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
config[:timestamp] = Time.now
|
81
|
+
MosEisley.logger.info('Config loaded')
|
46
82
|
return true
|
47
83
|
end
|
48
84
|
|
49
|
-
def self.apigw_event(event)
|
50
|
-
se =
|
85
|
+
def self.apigw_event(event, context)
|
86
|
+
se = MosEisley::SlackEvent.validate(event)
|
51
87
|
unless se[:valid?]
|
52
88
|
MosEisley.logger.warn("#{se[:msg]}")
|
53
89
|
return {statusCode: 401}
|
@@ -57,23 +93,21 @@ module MosEisley
|
|
57
93
|
MosEisley.logger.debug("Inbound Slack request to: #{ep}")
|
58
94
|
case ep
|
59
95
|
when '/actions'
|
60
|
-
|
96
|
+
## Slack Interactivity & Shortcuts
|
97
|
+
# Nothing to do, through-pass data
|
61
98
|
when '/commands'
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
else
|
70
|
-
text = sep[:text].empty? ? '' : " #{se[:event][:text]}"
|
71
|
-
"Received: `#{se[:event][:command]}#{text}`"
|
72
|
-
end
|
99
|
+
## Slack Slash Commands
|
100
|
+
MosEisley.logger.debug("Slash command event:\n#{se[:event]}")
|
101
|
+
r = MosEisley::Handler.run(:command_response, se[:event])
|
102
|
+
if String === r
|
103
|
+
r = {text: r}
|
104
|
+
end
|
105
|
+
if Hash === r
|
73
106
|
# AWS sets status code and headers by passing JSON string
|
74
|
-
resp = JSON.fast_generate(
|
107
|
+
resp = JSON.fast_generate(r)
|
75
108
|
end
|
76
109
|
when '/events'
|
110
|
+
## Slack Event Subscriptions
|
77
111
|
# Respond to Slack challenge request
|
78
112
|
if se[:event][:type] == 'url_verification'
|
79
113
|
c = se[:event][:challenge]
|
@@ -81,65 +115,55 @@ module MosEisley
|
|
81
115
|
return "{\"challenge\": \"#{c}\"}"
|
82
116
|
end
|
83
117
|
when '/menus'
|
84
|
-
#
|
118
|
+
# MosEisley::Handler.run(:menu, se)
|
85
119
|
# TODO to be implemented
|
86
120
|
return "{\"options\": []}"
|
87
121
|
else
|
88
122
|
MosEisley.logger.warn('Unknown request, ignored.')
|
89
123
|
return {statusCode: 400}
|
90
124
|
end
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
}
|
98
|
-
|
99
|
-
data_type: 'String',
|
100
|
-
string_value: 'moseisley',
|
101
|
-
},
|
102
|
-
'endpoint' => {
|
103
|
-
data_type: 'String',
|
104
|
-
string_value: ep,
|
105
|
-
},
|
106
|
-
},
|
107
|
-
message_body: "{\"payload\":#{se[:json]}}",
|
125
|
+
pl = {
|
126
|
+
Records: [
|
127
|
+
{
|
128
|
+
eventSource: 'MosEisley:Slack_event',
|
129
|
+
endpoint: ep,
|
130
|
+
body: se[:json],
|
131
|
+
}
|
132
|
+
]
|
108
133
|
}
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
134
|
+
lc = Aws::Lambda::Client.new
|
135
|
+
params = {
|
136
|
+
function_name: context.function_name,
|
137
|
+
invocation_type: 'Event',
|
138
|
+
payload: JSON.fast_generate(pl),
|
139
|
+
}
|
140
|
+
r = lc.invoke(params)
|
141
|
+
if r.status_code >= 200 && r.status_code < 300
|
142
|
+
MosEisley.logger.debug("Successfullly invoked with playload size: #{params[:payload].length}")
|
143
|
+
else
|
144
|
+
MosEisley.logger.warn("Problem with invoke, status code: #{r.status_code}")
|
145
|
+
end
|
146
|
+
resp
|
113
147
|
end
|
114
148
|
|
115
|
-
def self.
|
116
|
-
|
117
|
-
src = a.dig('source','stringValue')
|
118
|
-
dst = a.dig('destination','stringValue')
|
119
|
-
ep = a.dig('endpoint','stringValue')
|
149
|
+
def self.invoke_event(event)
|
150
|
+
ep = event.dig('Records',0,'endpoint')
|
120
151
|
se = JSON.parse(event.dig('Records',0,'body'), {symbolize_names: true})
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
when '/events'
|
131
|
-
ME::Handler.run(:event, se)
|
132
|
-
when '/menus'
|
133
|
-
MosEisley.logger.warn('Menu request cannot be processed here.')
|
134
|
-
else
|
135
|
-
MosEisley.logger.warn("Unknown request: #{ep}")
|
136
|
-
end
|
137
|
-
elsif dst == 'slack'
|
138
|
-
# An event to be sent to Slack
|
139
|
-
MosEisley.logger.debug a.dig('api','stringValue')
|
152
|
+
case ep
|
153
|
+
when '/actions'
|
154
|
+
MosEisley::Handler.run(:action, se)
|
155
|
+
when '/commands'
|
156
|
+
MosEisley::Handler.run(:command, se)
|
157
|
+
when '/events'
|
158
|
+
MosEisley::Handler.run(:event, se)
|
159
|
+
when '/menus'
|
160
|
+
MosEisley.logger.warn('Menu request cannot be processed here.')
|
140
161
|
else
|
141
|
-
MosEisley.logger.warn(
|
162
|
+
MosEisley.logger.warn("Unknown request: #{ep}")
|
142
163
|
end
|
143
|
-
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.unknown_event(event)
|
167
|
+
# TODO hand off to a handler
|
144
168
|
end
|
145
169
|
end
|
data/lib/s3po/blockkit.rb
CHANGED
@@ -1,6 +1,32 @@
|
|
1
|
+
# S3PO - Slack protocol droid in Mos Eisley
|
2
|
+
# ::BlockKit - Block Kit tools
|
3
|
+
#
|
4
|
+
# v.20220201
|
5
|
+
|
1
6
|
module MosEisley
|
2
7
|
module S3PO
|
3
8
|
module BlockKit
|
9
|
+
# @param txt [String]
|
10
|
+
# @param type [Symbol] :plain | :emoji | :mrkdwn
|
11
|
+
# @return [Hash] Block Kit section object
|
12
|
+
def self.con_text(txt, type = :mrkdwn)
|
13
|
+
{
|
14
|
+
type: :context,
|
15
|
+
elements: [
|
16
|
+
text(txt, type),
|
17
|
+
]
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param txt [String]
|
22
|
+
# @return [Hash] Block Kit header object
|
23
|
+
def self.header(txt)
|
24
|
+
{
|
25
|
+
type: :header,
|
26
|
+
text: text(txt, :emoji),
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
4
30
|
# @param txt [String]
|
5
31
|
# @param type [Symbol] :plain | :emoji | :mrkdwn
|
6
32
|
# @return [Hash] Block Kit section object
|
data/lib/s3po/s3po.rb
CHANGED
data/lib/slack.rb
CHANGED
@@ -15,7 +15,7 @@ module MosEisley
|
|
15
15
|
end
|
16
16
|
b = e['isBase64Encoded'] ? Base64.decode64(e['body']) : e['body']
|
17
17
|
s = "v0:#{t}:#{b}"
|
18
|
-
k =
|
18
|
+
k = MosEisley.config[:signing_secret]
|
19
19
|
sig = "v0=#{OpenSSL::HMAC.hexdigest('sha256', k, s)}"
|
20
20
|
if e.dig('headers', 'x-slack-signature') != sig
|
21
21
|
return {valid?: false, msg: 'Invalid signature.'}
|
@@ -155,7 +155,7 @@ module MosEisley
|
|
155
155
|
def self.get_from_slack(m, params)
|
156
156
|
l = MosEisley.logger
|
157
157
|
url ||= BASE_URL + m
|
158
|
-
head = {authorization: "Bearer #{
|
158
|
+
head = {authorization: "Bearer #{MosEisley.config[:bot_access_token]}"}
|
159
159
|
r = Neko::HTTP.get(url, params, head)
|
160
160
|
if r[:code] != 200
|
161
161
|
l.warn("#{m} HTTP failed: #{r[:message]}")
|
@@ -178,7 +178,7 @@ module MosEisley
|
|
178
178
|
def self.post_to_slack(method, data, url = nil)
|
179
179
|
l = MosEisley.logger
|
180
180
|
url ||= BASE_URL + method
|
181
|
-
head = {authorization: "Bearer #{
|
181
|
+
head = {authorization: "Bearer #{MosEisley.config[:bot_access_token]}"}
|
182
182
|
r = Neko::HTTP.post_json(url, data, head)
|
183
183
|
if r[:code] != 200
|
184
184
|
l.warn("post_to_slack HTTP failed: #{r[:message]}")
|
data/mos-eisley-lambda.gemspec
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'mos-eisley-lambda'
|
3
|
-
s.version = '0.
|
3
|
+
s.version = '0.6.0'
|
4
4
|
s.authors = ['Ken J.']
|
5
5
|
s.email = ['kenjij@gmail.com']
|
6
6
|
s.summary = %q{Ruby based Slack bot framework, for AWS Lambda use}
|
7
|
-
s.description = %q{Ruby based Slack bot framework, for AWS Lambda
|
7
|
+
s.description = %q{Ruby based Slack bot framework, for AWS Lambda; event queue based. Also provides Block Kit helper.}
|
8
8
|
s.homepage = 'https://github.com/kenjij/mos-eisley-lambda'
|
9
9
|
s.license = 'MIT'
|
10
10
|
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mos-eisley-lambda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ken J.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description: Ruby based Slack bot framework, for AWS Lambda
|
14
|
-
|
13
|
+
description: Ruby based Slack bot framework, for AWS Lambda; event queue based. Also
|
14
|
+
provides Block Kit helper.
|
15
15
|
email:
|
16
16
|
- kenjij@gmail.com
|
17
17
|
executables: []
|
@@ -49,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
49
|
- !ruby/object:Gem::Version
|
50
50
|
version: '0'
|
51
51
|
requirements: []
|
52
|
-
rubygems_version: 3.1.
|
52
|
+
rubygems_version: 3.1.4
|
53
53
|
signing_key:
|
54
54
|
specification_version: 4
|
55
55
|
summary: Ruby based Slack bot framework, for AWS Lambda use
|