mos-eisley-lambda 0.5.1 → 0.6.0

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