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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93e80b178ebf50a338825f24796cd4a940360ade9c5cb4e86febddc8c698892e
4
- data.tar.gz: fd8899967cb3969f38f2b51e2c6e4482185a7b9b690b8201aa28279e008ae0b7
3
+ metadata.gz: 4e38c9c1ecdcab343fc118677a1f7b515d4fe4ce19a0b4a056b50f515f81f87b
4
+ data.tar.gz: a0b90f7bfb4c67bf383ce059fab48a7ce93d0588bd0766603dc879ea14783789
5
5
  SHA512:
6
- metadata.gz: 31260b991c3adfd3b83a88b67f10c045a5ae6caab05a49bceff69e946230a953af0a55c8107e4ab7026bb8d4b10f7fd66c183164577c09369377fb78886ad1d7
7
- data.tar.gz: 2b8a6b426911626cdc93827638f8057aebac906925307eaf1d3488dab00c73ec61c664e6f270bd2b34ee80e4ee6982b27dce22cd91f5e0278ed0c76e601bdf30
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 ruby, no external dependency.
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
- - `SLACK_SIGNING_SECRET`: your Slack app credentials
24
- - `SLACK_BOT_ACCESS_TOKEN`: your Slack app OAuth token
25
- - `MOSEISLEY_SQS_URL`: AWS SQS URL used for the event pipeline
26
- - `MOSEISLEY_LOG_LEVEL` – optional, could be `DEBUG`, `INFO`, `WARN`, or `ERROR`
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 returned with parsed object
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 SQS with attributes
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. Slack event is recieved by SQS trigger
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. Should send a Slack message to complete the event cycle
106
+ 1. Send a Slack message as necessary and the Slack event cycle is complete
120
107
 
121
- <!-- ### Message Publishing
108
+ <!-- ### Outbound, Messaging Only
122
109
 
123
- Send a message to SQS from another app to send a Slack message
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 send to SQS
126
- 1. Message event is recieved by SQS trigger
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
- @handlers[type] << MosEisley::Handler.new(type, name, &block)
32
- MosEisley.logger.debug("Added #{type} handler: #{@handlers[type].last}")
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
- @handlers[type].each do |h|
52
- response = h.run(event)
53
- if h.stopped?
54
- logger.debug('Handler stop was requested.')
55
- break
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
 
@@ -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-sqs'
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.lambda_event(event)
14
- abort unless preflightcheck
15
- # Inbound Slack event (via API GW)
16
- if event['routeKey']
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
- end
20
- # Internal event (via SQS)
21
- if event.dig('Records',0,'eventSource') == 'aws:sqs'
22
- MosEisley.logger.info('SQS event')
23
- return sqs_event(event)
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
- l = ENV['MOSEISLEY_LOG_LEVEL']
29
- if String === l && ['DEBUG', 'INFO', 'WARN', 'ERROR'].include?(l.upcase)
30
- MosEisley.logger.level = eval("Logger::#{l.upcase}")
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
- 'MOSEISLEY_SQS_URL',
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
- env_required.each do |e|
41
- if ENV[e].nil?
42
- MosEisley.logger.error("Missing environment variable: #{e}")
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 = ME::SlackEvent.validate(event)
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
- # Nothing to do, just pass to SQS
96
+ ## Slack Interactivity & Shortcuts
97
+ # Nothing to do, through-pass data
61
98
  when '/commands'
62
- ser = {}
63
- ack = ME::Handler.command_acks[se[:event][:command]]
64
- if ack
65
- ser[:response_type] = ack[:response_type]
66
- ser[:text] =
67
- if ack[:text]
68
- ack[:text]
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(ser)
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
- # ME::Handler.run(:menu, se)
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
- m = {
92
- queue_url: ENV['MOSEISLEY_SQS_URL'],
93
- message_attributes: {
94
- 'source' => {
95
- data_type: 'String',
96
- string_value: 'slack',
97
- },
98
- 'destination' => {
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
- SQS.send_message(m)
110
- s = m[:message_body].length
111
- MosEisley.logger.debug("Sent message to SQS with body size #{s}.")
112
- return resp
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.sqs_event(event)
116
- a = event.dig('Records',0,'messageAttributes')
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
- se = se[:payload]
122
- MosEisley.logger.debug("Event src: #{src}, dst: #{dst}")
123
- if src == 'slack'
124
- # Inbound Slack event
125
- case ep
126
- when '/actions'
127
- ME::Handler.run(:action, se)
128
- when '/commands'
129
- ME::Handler.run(:command, se)
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('Unknown event, ignored.')
162
+ MosEisley.logger.warn("Unknown request: #{ep}")
142
163
  end
143
- return 0
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
@@ -1,3 +1,7 @@
1
+ # S3PO - Slack protocol droid in Mos Eisley
2
+ #
3
+ # v.20210626
4
+
1
5
  require 'json'
2
6
  require 'time'
3
7
  require_relative './blockkit'
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 = ENV['SLACK_SIGNING_SECRET']
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 #{ENV['SLACK_BOT_ACCESS_TOKEN']}"}
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 #{ENV['SLACK_BOT_ACCESS_TOKEN']}"}
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]}")
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'mos-eisley-lambda'
3
- s.version = '0.5.1'
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 use. Also provides helpers to analyze/build messages. Event based utilizing SQS.}
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.5.1
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: 2021-08-18 00:00:00.000000000 Z
11
+ date: 2022-02-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Ruby based Slack bot framework, for AWS Lambda use. Also provides helpers
14
- to analyze/build messages. Event based utilizing SQS.
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.2
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