mos-eisley-lambda 0.5.0 → 0.7.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: 7f47807ff37bc9efd5d01b8745fc9cfb36796876b03dc50cf5b1f5504d75cfb5
4
- data.tar.gz: a9fef8a47a8f9a7c2a7ca36122f047ecfef781f019e40bbcbf5f7eb6d423861d
3
+ metadata.gz: 1c7bb756dd5b280ff9fb49dc6d7a52776d1e86eea6d0295585317d8bcf36c079
4
+ data.tar.gz: 3747893c11d3255af90bc925700020ce8b4be4b77c2fd29c9de009c3e436bb8e
5
5
  SHA512:
6
- metadata.gz: 19c4a11da675e07f0cd1fd405dbdd1013be87086ad3f93afad9c85f64de9bbc6620beb4a6ff441892ca1590b0e9e3977bfbb2cc343e82b3868ec9bd7a804ec42
7
- data.tar.gz: 4c0421f4ad6e9655845c11fac9e981e2cb7f44856744a839df504f314947afdc888e475e801aa30572815495fba9da879fb4dc867ece6e52ab1b9fc002dcb462
6
+ metadata.gz: 12d43754b4fafe23f9b83997b014a3884fecda7d269f65e10ef33799bcf7f9dce20748196252757b2cffe2cb3b3e6a9f82ced3cab5aa32b6d90ed2c6a2bcb2ca
7
+ data.tar.gz: 88801d7784949fdb5cedb9ff44cfcac774447ade46c844f9c23ff59b83a42e8615d1c989928520b680a9d4022cec81037fc958fa8e426c3caeca52360c4dbae0
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,12 @@ 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_HANDLERS_DIR`: _optional_, if other than `./handlers`
26
+ - `MOSEISLEY_LOG_LEVEL`: _optional_, could be `DEBUG`, `INFO`, `WARN`, or `ERROR`
27
+ - `SLACK_LOG_CHANNEL_ID`: _optional_, if you want to use `ME::SlackWeb.post_log()`
27
28
 
28
29
  Configure Lambda code in your `lambda_function.rb` file.
29
30
 
@@ -32,12 +33,8 @@ require 'mos-eisley-lambda'
32
33
  # Or, you can just copy the `lib` directory to your Lambda and...
33
34
  # require_relative './lib/mos-eisley-lambda'
34
35
 
35
- MosEisley::Handler.import
36
- # Or, if you store your handlers in a non-default location, dictate by...
37
- # MosEisley::Handler.import_from_path('./my-handlers')
38
-
39
36
  def lambda_handler(event:, context:)
40
- MosEisley::lambda_event(event)
37
+ MosEisley::lambda_event(event, context)
41
38
  end
42
39
  ```
43
40
 
@@ -52,21 +49,26 @@ Create a Slack app and configure the following.
52
49
 
53
50
  ### Handlers
54
51
 
55
- Create your own Mos Eisley handlers as blocks and register them. By default, store these Ruby files in the `handlers` directory.
52
+ Create your own Mos Eisley handlers as blocks and register them. By default, store these Ruby files in the `handlers` directory. Add handlers by passing a block to `MosEisley::Handler.add()` for the types below.
53
+
54
+ ```ruby
55
+ :action
56
+ :command_response
57
+ :command
58
+ :event
59
+ :menu
60
+ :nonslack
61
+ ```
56
62
 
57
- `ME::Handler.command_acks` holds `[Hash<String, Hash>]` which are Slack command keyword and response pair. The response is sent as-is back to Slack as an [immediate response](https://api.slack.com/interactivity/slash-commands#responding_immediate_response).
63
+ `:command_response` types are Slack command keyword and response pair. The response is sent as-is back to Slack as an [immediate response](https://api.slack.com/interactivity/slash-commands#responding_immediate_response). `ME` is an alias to `MosEisley`.
58
64
 
59
65
  ```ruby
60
- ME::Handler.command_acks.merge!({
61
- '/command' => {
62
- response_type: 'in_channel',
63
- text: '_Working on it…_',
64
- },
65
- '/secret' => {
66
- response_type: 'ephemeral',
67
- text: '_Just for you…_',
68
- },
69
- })
66
+ ME::Handler.add(:command_response, '/sample') do |event, myself|
67
+ {
68
+ response_type: "in_channel",
69
+ text: "_Working on `#{event[:command]}`..._",
70
+ }
71
+ end
70
72
  ```
71
73
 
72
74
  Add handlers to process the Slack event.
@@ -85,46 +87,55 @@ ME::Handler.add(:command, 'A Slack command') do |event, myself|
85
87
  end
86
88
  ```
87
89
 
88
- ## Protocols
89
-
90
- ### SQS
90
+ If your function receives non-Slack events, you can add handlers for that as well.
91
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
92
+ ```ruby
93
+ ME::Handler.add(:nonslack, 'A CloudWatch event') do |event, myself|
94
+ next unless event['source'] == 'aws.events'
95
+ myself.stop
96
+ channel = 'C123SLCK'
97
+ txt = 'Shceduled event was received.'
98
+ ME::SlackWeb.chat_postmessage(channel: channel, text: txt)
99
+ end
100
+ ```
100
101
 
101
102
  ### Helpers
102
103
 
103
- - `ME::S3PO` – collection of helpers to analyze/create Slack messages.
104
- - `ME::SlackWeb` – methods for sending payloads to Slack Web API calls.
104
+ - `MosEisley::S3PO` – collection of helpers to analyze/create Slack messages.
105
+ - `MosEisley::SlackWeb` – methods for sending payloads to Slack Web API calls.
105
106
 
106
107
  ## Event Lifecycle
107
108
 
108
109
  ### Inbound
109
110
 
110
- 1. Slack event is sent to Mos Eisley Lambda function via API Gateway
111
- 1. Slack event is verified and returned with parsed object
112
- 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
114
-
115
- ### Event Processing
116
-
117
- 1. Slack event is recieved by SQS trigger
118
- 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
111
+ To an incoming Slack event, Mos Eisley will quickly respond with a blank HTTP 200. This is to keep [Slack's 3-second rule](https://api.slack.com/apis/connections/events-api#the-events-api__responding-to-events). To do this, handlers are not called yet, but the Slack event is passed on to a recursive asynchronous invoke and then the handlers are called.
112
+
113
+ The exception is when the incoming Slack event is for a slash command. You can define `:command_response` handlers for the purpose of generating a simple response message, but nothing more.
114
+
115
+ ```mermaid
116
+ sequenceDiagram
117
+ participant S as Slack
118
+ participant L as Lambda MosEisley
119
+ S->>+L: Slack event via API Gateway
120
+ alt Slash command
121
+ L-->>S: Response message
122
+ Note left of L: If a response handler is defined
123
+ else All other events
124
+ L-->>-S: HTTP 200 (blank)
125
+ end
126
+ L->>+L: Slack event
127
+ Note right of L: Handlers are called
128
+ opt
129
+ L-->>-S: E.g., chat.postMessage
130
+ end
131
+ ```
120
132
 
121
- <!-- ### Message Publishing
133
+ <!-- ### Outbound, Messaging Only
122
134
 
123
- Send a message to SQS from another app to send a Slack message
135
+ Invoke the function from another app to send a Slack message
124
136
 
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 -->
137
+ 1. Create a Slack message packaged to be sent to the API and invoke the function
138
+ 1. Message is received, then sent to Slack API according to payload-->
128
139
 
129
140
  ## Using with Lambda Layers
130
141
 
@@ -0,0 +1,55 @@
1
+ ##
2
+ ## Sample handlers for Mos Eisley
3
+ ##
4
+ ME::Handler.add(:event, 'DEBUG') do |event, myself|
5
+ l = ME.logger
6
+ l.debug("[Slack-Event]\n#{event}")
7
+ end
8
+
9
+ ME::Handler.add(:event, 'Request - diagnostics') do |event, myself|
10
+ se = event[:event]
11
+ next unless se[:type] == 'app_mention' && /\bdiag/i =~ se[:text]
12
+ myself.stop
13
+ l = ME.logger
14
+ bk = ME::S3PO::BlockKit
15
+ fs = []
16
+ ME.config.info[:handlers].each{ |k, v| fs << "*#{k}*\n#{v}" }
17
+ blks = [
18
+ bk.sec_text('Handler Count'),
19
+ bk.sec_fields(fs),
20
+ ]
21
+ fs = []
22
+ ME.config.info[:versions].each{ |k, v| fs << "*#{k}*\n#{v}" }
23
+ blks << bk.sec_text('Software Versions')
24
+ blks << bk.sec_fields(fs)
25
+ ME::SlackWeb.chat_postmessage(channel: se[:channel], text: "Diagnostics", blocks: blks)
26
+ end
27
+
28
+ ME::Handler.add(:nonslack, 'DEBUG') do |event, myself|
29
+ l = ME.logger
30
+ l.debug("[Non-Slack]\n#{event}")
31
+ end
32
+
33
+ ME::Handler.add(:command_response, '/sample') do |event, myself|
34
+ {
35
+ response_type: "in_channel",
36
+ text: "_Working on `#{event[:command]}`..._",
37
+ }
38
+ end
39
+
40
+ ME::Handler.add(:command, 'DEBUG') do |event, myself|
41
+ l = ME.logger
42
+ l.debug("[Slack-Command]\n#{event}")
43
+ end
44
+
45
+ ME::Handler.add(:command, 'Request - /sample') do |event, myself|
46
+ next unless event[:command] == '/sample'
47
+ myself.stop
48
+ bk = ME::S3PO::BlockKit
49
+ t = "`S A M P L E` I did it!"
50
+ blks = [
51
+ bk.sec_text(t),
52
+ bk.con_text('By: Mos Eisley sampler'),
53
+ ]
54
+ ME::SlackWeb.chat_postmessage(channel: event[:command], text: t, blocks: blks)
55
+ end
data/lib/handler.rb CHANGED
@@ -19,23 +19,27 @@ 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: [],
34
+ nonslack: [],
30
35
  }
31
- @handlers[type] << MosEisley::Handler.new(type, name, &block)
32
- MosEisley.logger.debug("Added #{type} handler: #{@handlers[type].last}")
33
- end
34
-
35
- # Example: {'/command' => {response_type: 'ephemeral', text: nil}}
36
- # @return [Hash<String, Hash>] commands to acknowledge
37
- def self.command_acks
38
- @command_acks ||= {}
36
+ h = MosEisley::Handler.new(type, name, &block)
37
+ if type == :command_response
38
+ @handlers[type][name] = h
39
+ else
40
+ @handlers[type] << h
41
+ end
42
+ MosEisley.logger.debug("Added handler: #{h}")
39
43
  end
40
44
 
41
45
  # @return [Hash<Symbol, Array>] containing all the handlers
@@ -48,14 +52,22 @@ module MosEisley
48
52
  def self.run(type, event)
49
53
  logger = MosEisley.logger
50
54
  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
55
+ if type == :command_response
56
+ h = @handlers[type][event[:command]]
57
+ if h
58
+ response = h.run(event)
59
+ logger.info("Done running #{type} handlers.")
60
+ end
61
+ else
62
+ @handlers[type].each do |h|
63
+ response = h.run(event)
64
+ if h.stopped?
65
+ logger.debug('Handler stop was requested.')
66
+ break
67
+ end
56
68
  end
69
+ logger.info("Done running #{type} handlers.")
57
70
  end
58
- logger.info("Done running #{type} handlers.")
59
71
  response
60
72
  end
61
73
 
@@ -89,7 +101,7 @@ module MosEisley
89
101
  end
90
102
 
91
103
  def to_s
92
- "#<#{self.class}:#{self.object_id.to_s(16)}(#{name})>"
104
+ "#<#{self.class}:#{self.object_id.to_s(16)}(#{type}:#{name})>"
93
105
  end
94
106
  end
95
107
  end
@@ -2,52 +2,111 @@ 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_relative './version'
6
+ require 'aws-sdk-lambda'
7
+ require 'aws-sdk-ssm'
6
8
  require 'base64'
7
9
  require 'json'
8
10
 
9
11
  ME = MosEisley
10
- SQS = Aws::SQS::Client.new
11
12
 
12
13
  module MosEisley
13
- def self.lambda_event(event)
14
- abort unless preflightcheck
15
- # Inbound Slack event (via API GW)
16
- if event['routeKey']
17
- MosEisley.logger.info('API GW event')
18
- return apigw_event(event)
14
+ def self.config(context = nil, data = nil)
15
+ if data
16
+ unless @config
17
+ @config = Config.new(context, data)
18
+ MosEisley.logger.info('Config loaded')
19
+ else
20
+ MosEisley.logger.warn('Ignored, already configured')
21
+ end
19
22
  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
+ @config
24
+ end
25
+
26
+ def self.lambda_event(event, context)
27
+ raise 'Pre-flight check failed!' unless preflightcheck(context)
28
+ case
29
+ when event['initializeOnly']
30
+ MosEisley.logger.info('Dry run, initializing only')
31
+ return
32
+ when event['routeKey']
33
+ # Inbound Slack event (via API GW)
34
+ MosEisley.logger.info('API GW event')
35
+ return apigw_event(event, context)
36
+ when event.dig('Records',0,'eventSource') == 'MosEisley:Slack_event'
37
+ # Internal event (via invoke)
38
+ MosEisley.logger.info('Invoke event')
39
+ MosEisley.logger.debug("#{event}")
40
+ return invoke_event(event)
41
+ when event.dig('Records',0,'eventSource').start_with?('MosEisley:Slack_message:')
42
+ # Outbound Slack messaging request (via invoke)
43
+ MosEisley.logger.info('Messaging event')
44
+ MosEisley.logger.debug("#{event}")
45
+ return # TODO implement
46
+ else
47
+ # Non-Slack event
48
+ MosEisley.logger.info('Non-Slack event')
49
+ return nonslack_event(event)
24
50
  end
25
51
  end
26
52
 
27
- 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}")
53
+ def self.preflightcheck(context)
54
+ if config
55
+ MosEisley.logger.debug("Confing already loaded at: #{config.timestamp}")
56
+ return true
31
57
  end
32
58
  env_required = [
33
- 'MOSEISLEY_SQS_URL',
34
- 'SLACK_SIGNING_SECRET',
35
- 'SLACK_BOT_ACCESS_TOKEN',
59
+ 'SLACK_CREDENTIALS_SSMPS_PATH',
36
60
  ]
37
61
  env_optional = [
62
+ 'MOSEISLEY_HANDLERS_DIR',
38
63
  'MOSEISLEY_LOG_LEVEL',
64
+ 'SLACK_LOG_CHANNEL_ID',
65
+ ]
66
+ config_required = [
67
+ :signing_secret,
68
+ :bot_access_token,
39
69
  ]
40
- env_required.each do |e|
41
- if ENV[e].nil?
42
- MosEisley.logger.error("Missing environment variable: #{e}")
70
+ l = ENV['MOSEISLEY_LOG_LEVEL']
71
+ if String === l && ['DEBUG', 'INFO', 'WARN', 'ERROR'].include?(l.upcase)
72
+ MosEisley.logger.level = eval("Logger::#{l.upcase}")
73
+ end
74
+ if dir = ENV['MOSEISLEY_HANDLERS_DIR']
75
+ MosEisley::Handler.import_from_path(dir)
76
+ else
77
+ MosEisley::Handler.import
78
+ end
79
+ env_required.each do |v|
80
+ if ENV[v].nil?
81
+ MosEisley.logger.error("Missing environment variable: #{v}")
43
82
  return false
44
83
  end
84
+ case v
85
+ when 'SLACK_CREDENTIALS_SSMPS_PATH'
86
+ ssm = Aws::SSM::Client.new
87
+ rparams = {
88
+ path: ENV['SLACK_CREDENTIALS_SSMPS_PATH'],
89
+ with_decryption: true,
90
+ }
91
+ c = {}
92
+ ssm.get_parameters_by_path(rparams).parameters.each do |prm|
93
+ k = prm[:name].split('/').last.to_sym
94
+ c[k] = prm[:value]
95
+ config_required.delete(k)
96
+ end
97
+ unless config_required.empty?
98
+ t = "Missing config values: #{config_required.join(', ')}"
99
+ MosEisley.logger.error(t)
100
+ return false
101
+ end
102
+ config(context, c)
103
+ end
45
104
  end
46
105
  return true
47
106
  end
48
107
 
49
- def self.apigw_event(event)
50
- se = ME::SlackEvent.validate(event)
108
+ def self.apigw_event(event, context)
109
+ se = MosEisley::SlackEvent.validate(event)
51
110
  unless se[:valid?]
52
111
  MosEisley.logger.warn("#{se[:msg]}")
53
112
  return {statusCode: 401}
@@ -57,23 +116,21 @@ module MosEisley
57
116
  MosEisley.logger.debug("Inbound Slack request to: #{ep}")
58
117
  case ep
59
118
  when '/actions'
60
- # Nothing to do, just pass to SQS
119
+ ## Slack Interactivity & Shortcuts
120
+ # Nothing to do, through-pass data
61
121
  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
122
+ ## Slack Slash Commands
123
+ MosEisley.logger.debug("Slash command event:\n#{se[:event]}")
124
+ r = MosEisley::Handler.run(:command_response, se[:event])
125
+ if String === r
126
+ r = {text: r}
127
+ end
128
+ if Hash === r
73
129
  # AWS sets status code and headers by passing JSON string
74
- resp = JSON.fast_generate(ser)
130
+ resp = JSON.fast_generate(r)
75
131
  end
76
132
  when '/events'
133
+ ## Slack Event Subscriptions
77
134
  # Respond to Slack challenge request
78
135
  if se[:event][:type] == 'url_verification'
79
136
  c = se[:event][:challenge]
@@ -81,65 +138,90 @@ module MosEisley
81
138
  return "{\"challenge\": \"#{c}\"}"
82
139
  end
83
140
  when '/menus'
84
- # ME::Handler.run(:menu, se)
141
+ # MosEisley::Handler.run(:menu, se)
85
142
  # TODO to be implemented
86
143
  return "{\"options\": []}"
87
144
  else
88
145
  MosEisley.logger.warn('Unknown request, ignored.')
89
146
  return {statusCode: 400}
90
147
  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]}}",
148
+ pl = {
149
+ Records: [
150
+ {
151
+ eventSource: 'MosEisley:Slack_event',
152
+ endpoint: ep,
153
+ body: se[:json],
154
+ }
155
+ ]
156
+ }
157
+ lc = Aws::Lambda::Client.new
158
+ params = {
159
+ function_name: context.function_name,
160
+ invocation_type: 'Event',
161
+ payload: JSON.fast_generate(pl),
108
162
  }
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
163
+ r = lc.invoke(params)
164
+ if r.status_code >= 200 && r.status_code < 300
165
+ MosEisley.logger.debug("Successfullly invoked with playload size: #{params[:payload].length}")
166
+ else
167
+ MosEisley.logger.warn("Problem with invoke, status code: #{r.status_code}")
168
+ end
169
+ resp
113
170
  end
114
171
 
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')
172
+ def self.invoke_event(event)
173
+ ep = event.dig('Records',0,'endpoint')
120
174
  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')
175
+ case ep
176
+ when '/actions'
177
+ MosEisley::Handler.run(:action, se)
178
+ when '/commands'
179
+ MosEisley::Handler.run(:command, se)
180
+ when '/events'
181
+ MosEisley::Handler.run(:event, se)
182
+ when '/menus'
183
+ MosEisley.logger.warn('Menu request cannot be processed here.')
140
184
  else
141
- MosEisley.logger.warn('Unknown event, ignored.')
185
+ MosEisley.logger.warn("Unknown request: #{ep}")
186
+ end
187
+ end
188
+
189
+ def self.nonslack_event(event)
190
+ MosEisley::Handler.run(:nonslack, event)
191
+ end
192
+
193
+ class Config
194
+ attr_reader :context, :info, :timestamp
195
+ attr_reader :bot_access_token, :signing_secret
196
+
197
+ def initialize(context, data)
198
+ data.each do |k, v|
199
+ instance_variable_set("@#{k}", v)
200
+ end
201
+ @context = context
202
+ @info = {
203
+ handlers: {
204
+ action: MosEisley.handlers[:action].length,
205
+ command_response: MosEisley.handlers[:command_response].length,
206
+ command: MosEisley.handlers[:command].length,
207
+ event: MosEisley.handlers[:event].length,
208
+ },
209
+ versions: {
210
+ mos_eisley: MosEisley::VERSION,
211
+ neko_http: Neko::HTTP::VERSION,
212
+ s3po: MosEisley::S3PO::VERSION,
213
+ s3po_blockkit: MosEisley::S3PO::BlockKit::VERSION,
214
+ },
215
+ }
216
+ @timestamp = Time.now
217
+ end
218
+
219
+ def arn
220
+ context.invoked_function_arn
221
+ end
222
+
223
+ def remaining_time
224
+ context.get_remaining_time_in_millis
142
225
  end
143
- return 0
144
226
  end
145
227
  end
data/lib/neko-http.rb CHANGED
@@ -1,7 +1,6 @@
1
+ #
1
2
  # NekoHTTP - Pure Ruby HTTP client using net/http
2
- #
3
- # v.20200629
4
-
3
+ #
5
4
  require 'json'
6
5
  require 'logger'
7
6
  require 'net/http'
@@ -17,6 +16,8 @@ module Neko
17
16
  end
18
17
 
19
18
  class HTTP
19
+ VERSION = '20220224'.freeze
20
+
20
21
  METHOD_HTTP_CLASS = {
21
22
  get: Net::HTTP::Get,
22
23
  put: Net::HTTP::Put,
@@ -25,6 +26,11 @@ module Neko
25
26
  delete: Net::HTTP::Delete
26
27
  }
27
28
 
29
+ # Simple GET request
30
+ # @param url [String] full URL string
31
+ # @param params [Array, Hash] it will be converted to URL encoded query
32
+ # @param hdrs [Hash] HTTP headers
33
+ # @return [Hash] contains: :code, :headers, :body, :message
28
34
  def self.get(url, params, hdrs = nil)
29
35
  h = HTTP.new(url, hdrs)
30
36
  data = h.get(params: params)
@@ -32,6 +38,11 @@ module Neko
32
38
  return data
33
39
  end
34
40
 
41
+ # Send POST request with form data URL encoded body
42
+ # @param url [String] full URL string
43
+ # @param params [Array, Hash] it will be converted to URL encoded body
44
+ # @param hdrs [Hash] HTTP headers
45
+ # @return (see #self.get)
35
46
  def self.post_form(url, params, hdrs = nil)
36
47
  h = HTTP.new(url, hdrs)
37
48
  data = h.post(params: params)
@@ -43,7 +54,8 @@ module Neko
43
54
  # It will set the Content-Type to application/json.
44
55
  # @param url [String] full URL string
45
56
  # @param obj [Array, Hash, String] Array/Hash will be converted to JSON
46
- # @param hdrs [Array, Hash, String] Array/Hash will be converted to JSON
57
+ # @param hdrs [Hash] HTTP headers
58
+ # @return (see #self.get)
47
59
  def self.post_json(url, obj, hdrs = {})
48
60
  hdrs['Content-Type'] = 'application/json'
49
61
  h = HTTP.new(url, hdrs)
@@ -63,6 +75,9 @@ module Neko
63
75
  attr_reader :init_uri, :http
64
76
  attr_accessor :logger, :headers
65
77
 
78
+ # Instance constructor for tailored use
79
+ # @param url [String] full URL string
80
+ # @param hdrs [Hash] HTTP headers
66
81
  def initialize(url, hdrs = nil)
67
82
  @logger = Neko.logger
68
83
  @init_uri = URI(url)
data/lib/s3po/blockkit.rb CHANGED
@@ -1,6 +1,33 @@
1
+ #
2
+ # S3PO - Slack protocol droid in Mos Eisley
3
+ # ::BlockKit - Block Kit tools
4
+ #
1
5
  module MosEisley
2
6
  module S3PO
3
7
  module BlockKit
8
+ VERSION = '20220224'.freeze
9
+
10
+ # @param txt [String]
11
+ # @param type [Symbol] :plain | :emoji | :mrkdwn
12
+ # @return [Hash] Block Kit section object
13
+ def self.con_text(txt, type = :mrkdwn)
14
+ {
15
+ type: :context,
16
+ elements: [
17
+ text(txt, type),
18
+ ]
19
+ }
20
+ end
21
+
22
+ # @param txt [String]
23
+ # @return [Hash] Block Kit header object
24
+ def self.header(txt)
25
+ {
26
+ type: :header,
27
+ text: text(txt, :emoji),
28
+ }
29
+ end
30
+
4
31
  # @param txt [String]
5
32
  # @param type [Symbol] :plain | :emoji | :mrkdwn
6
33
  # @return [Hash] Block Kit section object
@@ -11,6 +38,16 @@ module MosEisley
11
38
  }
12
39
  end
13
40
 
41
+ # @param fields [Array<String>]
42
+ # @param type [Symbol] :plain | :emoji | :mrkdwn
43
+ # @return [Hash] Block Kit section object
44
+ def self.sec_fields(fields, type = :mrkdwn)
45
+ {
46
+ type: :section,
47
+ fields: fields.map{ |txt| text(txt, type) },
48
+ }
49
+ end
50
+
14
51
  # @param txt [String]
15
52
  # @param type [Symbol] :plain | :emoji | :mrkdwn
16
53
  # @return [Hash] Block Kit text object
data/lib/s3po/s3po.rb CHANGED
@@ -1,9 +1,14 @@
1
+ #
2
+ # S3PO - Slack protocol droid in Mos Eisley
3
+ #
1
4
  require 'json'
2
5
  require 'time'
3
6
  require_relative './blockkit'
4
7
 
5
8
  module MosEisley
6
9
  module S3PO
10
+ VERSION = '20210626'.freeze
11
+
7
12
  def self.parse_json(json)
8
13
  return JSON.parse(json, {symbolize_names: true})
9
14
  rescue => e
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.'}
@@ -120,33 +120,45 @@ module MosEisley
120
120
  def self.views_push(trigger_id:, view:)
121
121
  end
122
122
 
123
+ def self.conversations_members(channel:, cursor: nil, limit: nil)
124
+ params = {channel: channel}
125
+ params[:cursor] = cursor if cursor
126
+ params[:limit] = limit if limit
127
+ get_from_slack('conversations.members', params)
128
+ end
129
+
123
130
  def self.users_info(user)
124
- users_send('info', {user: user})
131
+ get_from_slack('users.info', {user: user})
125
132
  end
126
133
 
127
134
  def self.users_list(cursor: nil, limit: nil)
128
135
  params = {include_locale: true}
129
136
  params[:cursor] = cursor if cursor
130
137
  params[:limit] = limit if limit
131
- users_send('list', params)
138
+ get_from_slack('users.list', params)
132
139
  end
133
140
 
134
141
  def self.users_lookupbyemail(email)
135
- users_send('lookupByEmail', {email: email})
142
+ get_from_slack('users.lookupByEmail', {email: email})
136
143
  end
137
144
 
138
145
  def self.users_profile_get(user)
139
- users_send('profile.get', {user: user})
146
+ get_from_slack('users.profile.get', {user: user})
147
+ end
148
+
149
+ def self.auth_test
150
+ post_to_slack('auth.test', '')
140
151
  end
141
152
 
142
- def self.users_send(m, params)
153
+ private
154
+
155
+ def self.get_from_slack(m, params)
143
156
  l = MosEisley.logger
144
- call = "users.#{m}"
145
- url ||= BASE_URL + call
146
- head = {authorization: "Bearer #{ENV['SLACK_BOT_ACCESS_TOKEN']}"}
157
+ url ||= BASE_URL + m
158
+ head = {authorization: "Bearer #{MosEisley.config.bot_access_token}"}
147
159
  r = Neko::HTTP.get(url, params, head)
148
160
  if r[:code] != 200
149
- l.warn("#{call} HTTP failed: #{r[:message]}")
161
+ l.warn("#{m} HTTP failed: #{r[:message]}")
150
162
  return nil
151
163
  end
152
164
  begin
@@ -154,7 +166,7 @@ module MosEisley
154
166
  if h[:ok]
155
167
  return h
156
168
  else
157
- l.warn("#{call} Slack failed: #{h[:error]}")
169
+ l.warn("#{m} Slack failed: #{h[:error]}")
158
170
  l.debug("#{h[:response_metadata]}")
159
171
  return nil
160
172
  end
@@ -163,16 +175,10 @@ module MosEisley
163
175
  end
164
176
  end
165
177
 
166
- def self.auth_test
167
- post_to_slack('auth.test', '')
168
- end
169
-
170
- private
171
-
172
178
  def self.post_to_slack(method, data, url = nil)
173
179
  l = MosEisley.logger
174
180
  url ||= BASE_URL + method
175
- head = {authorization: "Bearer #{ENV['SLACK_BOT_ACCESS_TOKEN']}"}
181
+ head = {authorization: "Bearer #{MosEisley.config.bot_access_token}"}
176
182
  r = Neko::HTTP.post_json(url, data, head)
177
183
  if r[:code] != 200
178
184
  l.warn("post_to_slack HTTP failed: #{r[:message]}")
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module MosEisley
2
+ VERSION = '0.7.0'.freeze
3
+ end
@@ -1,10 +1,12 @@
1
+ require_relative './lib/version'
2
+
1
3
  Gem::Specification.new do |s|
2
4
  s.name = 'mos-eisley-lambda'
3
- s.version = '0.5.0'
5
+ s.version = MosEisley::VERSION
4
6
  s.authors = ['Ken J.']
5
7
  s.email = ['kenjij@gmail.com']
6
8
  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.}
9
+ s.description = %q{Ruby based Slack bot framework, for AWS Lambda; event queue based. Also provides Block Kit helper.}
8
10
  s.homepage = 'https://github.com/kenjij/mos-eisley-lambda'
9
11
  s.license = 'MIT'
10
12
 
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.0
4
+ version: 0.7.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-14 00:00:00.000000000 Z
11
+ date: 2022-03-03 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: []
@@ -21,6 +21,7 @@ files:
21
21
  - LICENSE
22
22
  - Makefile
23
23
  - README.md
24
+ - handlers/sample.rb
24
25
  - lib/handler.rb
25
26
  - lib/logger.rb
26
27
  - lib/mos-eisley-lambda.rb
@@ -28,6 +29,7 @@ files:
28
29
  - lib/s3po/blockkit.rb
29
30
  - lib/s3po/s3po.rb
30
31
  - lib/slack.rb
32
+ - lib/version.rb
31
33
  - mos-eisley-lambda.gemspec
32
34
  - openapi3.yaml
33
35
  homepage: https://github.com/kenjij/mos-eisley-lambda
@@ -49,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
51
  - !ruby/object:Gem::Version
50
52
  version: '0'
51
53
  requirements: []
52
- rubygems_version: 3.1.2
54
+ rubygems_version: 3.1.4
53
55
  signing_key:
54
56
  specification_version: 4
55
57
  summary: Ruby based Slack bot framework, for AWS Lambda use