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