mos-eisley-lambda 0.5.0 → 0.7.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: 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