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 +4 -4
- data/README.md +62 -51
- data/handlers/sample.rb +55 -0
- data/lib/handler.rb +29 -17
- data/lib/mos-eisley-lambda.rb +167 -85
- data/lib/neko-http.rb +19 -4
- data/lib/s3po/blockkit.rb +37 -0
- data/lib/s3po/s3po.rb +5 -0
- data/lib/slack.rb +24 -18
- data/lib/version.rb +3 -0
- data/mos-eisley-lambda.gemspec +4 -2
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c7bb756dd5b280ff9fb49dc6d7a52776d1e86eea6d0295585317d8bcf36c079
|
4
|
+
data.tar.gz: 3747893c11d3255af90bc925700020ce8b4be4b77c2fd29c9de009c3e436bb8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
- `
|
24
|
-
- `
|
25
|
-
- `
|
26
|
-
- `
|
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
|
-
`
|
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.
|
61
|
-
|
62
|
-
response_type:
|
63
|
-
text:
|
64
|
-
}
|
65
|
-
|
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
|
-
|
89
|
-
|
90
|
-
### SQS
|
90
|
+
If your function receives non-Slack events, you can add handlers for that as well.
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
- `
|
104
|
-
- `
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
<!-- ###
|
133
|
+
<!-- ### Outbound, Messaging Only
|
122
134
|
|
123
|
-
|
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
|
126
|
-
1. Message
|
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
|
|
data/handlers/sample.rb
ADDED
@@ -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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
52
|
-
|
53
|
-
if h
|
54
|
-
|
55
|
-
|
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
|
data/lib/mos-eisley-lambda.rb
CHANGED
@@ -2,52 +2,111 @@ require_relative './logger'
|
|
2
2
|
require_relative './slack'
|
3
3
|
require_relative './s3po/s3po'
|
4
4
|
require_relative './handler'
|
5
|
-
|
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.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
'
|
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
|
-
|
41
|
-
|
42
|
-
|
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 =
|
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
|
-
|
119
|
+
## Slack Interactivity & Shortcuts
|
120
|
+
# Nothing to do, through-pass data
|
61
121
|
when '/commands'
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
else
|
70
|
-
text = sep[:text].empty? ? '' : " #{se[:event][:text]}"
|
71
|
-
"Received: `#{se[:event][:command]}#{text}`"
|
72
|
-
end
|
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(
|
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
|
-
#
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
}
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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.
|
116
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
when '/events'
|
131
|
-
ME::Handler.run(:event, se)
|
132
|
-
when '/menus'
|
133
|
-
MosEisley.logger.warn('Menu request cannot be processed here.')
|
134
|
-
else
|
135
|
-
MosEisley.logger.warn("Unknown request: #{ep}")
|
136
|
-
end
|
137
|
-
elsif dst == 'slack'
|
138
|
-
# An event to be sent to Slack
|
139
|
-
MosEisley.logger.debug a.dig('api','stringValue')
|
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(
|
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 [
|
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 =
|
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
|
-
|
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
|
-
|
138
|
+
get_from_slack('users.list', params)
|
132
139
|
end
|
133
140
|
|
134
141
|
def self.users_lookupbyemail(email)
|
135
|
-
|
142
|
+
get_from_slack('users.lookupByEmail', {email: email})
|
136
143
|
end
|
137
144
|
|
138
145
|
def self.users_profile_get(user)
|
139
|
-
|
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
|
-
|
153
|
+
private
|
154
|
+
|
155
|
+
def self.get_from_slack(m, params)
|
143
156
|
l = MosEisley.logger
|
144
|
-
|
145
|
-
|
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("#{
|
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("#{
|
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 #{
|
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
data/mos-eisley-lambda.gemspec
CHANGED
@@ -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 =
|
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
|
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.
|
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:
|
11
|
+
date: 2022-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description: Ruby based Slack bot framework, for AWS Lambda
|
14
|
-
|
13
|
+
description: Ruby based Slack bot framework, for AWS Lambda; event queue based. Also
|
14
|
+
provides Block Kit helper.
|
15
15
|
email:
|
16
16
|
- kenjij@gmail.com
|
17
17
|
executables: []
|
@@ -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.
|
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
|