waylon-slack 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 909809c1f1e6121fb84667fb922a54cf22cbd801191ab646c9c9db5305c87e60
4
+ data.tar.gz: d776dd6a040f8278862e8df2c176254acac8a2695e4965e8c06c3b56daef57e0
5
+ SHA512:
6
+ metadata.gz: 56e572e2fbc880b4ce64f5a61ba888712282e54334705b07d8ecb671d8064fd5b2c217b7f789979bbc1e64ec09ca204b01e301b0b5c1d22766f74a6997c19661
7
+ data.tar.gz: 5bfeadf428a8bc725c058955a5c0ca4987af49940d7c6523a02b8644e197ff495289f81aa8ea9fd9ef03d2126fa3431ec7ff9db46c5c2db2607f527bf4d33499
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # Rackup testing in this repo
11
+ *.ru
12
+
13
+ # rspec failure tracking
14
+ .rspec_status
data/.roxanne.yml ADDED
@@ -0,0 +1,4 @@
1
+ version: 1.0
2
+ stages:
3
+ test:
4
+ image: ruby:3.1
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,38 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+ NewCops: enable
4
+
5
+ Style/MixinUsage:
6
+ Exclude:
7
+ - "bin/console"
8
+
9
+ Style/StringLiterals:
10
+ Enabled: true
11
+ EnforcedStyle: double_quotes
12
+
13
+ Style/StringLiteralsInInterpolation:
14
+ Enabled: true
15
+ EnforcedStyle: double_quotes
16
+
17
+ Layout/LineLength:
18
+ Max: 120
19
+
20
+ Gemspec/RequireMFA:
21
+ Enabled: false
22
+
23
+ Metrics/AbcSize:
24
+ Max: 19
25
+
26
+ Metrics/CyclomaticComplexity:
27
+ Max: 9
28
+
29
+ Metrics/PerceivedComplexity:
30
+ Max: 9
31
+
32
+ Metrics/MethodLength:
33
+ Max: 20
34
+
35
+ Metrics/BlockLength:
36
+ Exclude:
37
+ - "**/*_spec.rb"
38
+ - "*.gemspec"
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-01-06
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in waylon-slack.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,157 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ waylon-slack (0.1.0)
5
+ slack-ruby-client (~> 1.0)
6
+ waylon-core (~> 0.1)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.8.0)
12
+ public_suffix (>= 2.0.2, < 5.0)
13
+ ast (2.4.2)
14
+ concurrent-ruby (1.1.9)
15
+ diff-lcs (1.5.0)
16
+ docile (1.4.0)
17
+ faraday (1.9.3)
18
+ faraday-em_http (~> 1.0)
19
+ faraday-em_synchrony (~> 1.0)
20
+ faraday-excon (~> 1.1)
21
+ faraday-httpclient (~> 1.0)
22
+ faraday-multipart (~> 1.0)
23
+ faraday-net_http (~> 1.0)
24
+ faraday-net_http_persistent (~> 1.0)
25
+ faraday-patron (~> 1.0)
26
+ faraday-rack (~> 1.0)
27
+ faraday-retry (~> 1.0)
28
+ ruby2_keywords (>= 0.0.4)
29
+ faraday-em_http (1.0.0)
30
+ faraday-em_synchrony (1.0.0)
31
+ faraday-excon (1.1.0)
32
+ faraday-httpclient (1.0.1)
33
+ faraday-multipart (1.0.3)
34
+ multipart-post (>= 1.2, < 3)
35
+ faraday-net_http (1.0.1)
36
+ faraday-net_http_persistent (1.2.0)
37
+ faraday-patron (1.0.0)
38
+ faraday-rack (1.0.0)
39
+ faraday-retry (1.0.3)
40
+ faraday_middleware (1.2.0)
41
+ faraday (~> 1.0)
42
+ gli (2.21.0)
43
+ hashie (5.0.0)
44
+ i18n (1.9.1)
45
+ concurrent-ruby (~> 1.0)
46
+ json (2.6.1)
47
+ moneta (1.4.2)
48
+ mono_logger (1.1.1)
49
+ multi_json (1.15.0)
50
+ multipart-post (2.1.1)
51
+ mustermann (1.1.1)
52
+ ruby2_keywords (~> 0.0.1)
53
+ nio4r (2.5.8)
54
+ parallel (1.21.0)
55
+ parser (3.1.0.0)
56
+ ast (~> 2.4.1)
57
+ public_suffix (4.0.6)
58
+ puma (5.6.1)
59
+ nio4r (~> 2.0)
60
+ rack (2.2.3)
61
+ rack-protection (2.1.0)
62
+ rack
63
+ rainbow (3.1.1)
64
+ rake (13.0.6)
65
+ redis (4.5.1)
66
+ redis-namespace (1.8.1)
67
+ redis (>= 3.0.4)
68
+ regexp_parser (2.2.0)
69
+ resque (2.2.0)
70
+ mono_logger (~> 1.0)
71
+ multi_json (~> 1.0)
72
+ redis-namespace (~> 1.6)
73
+ sinatra (>= 0.9.2)
74
+ vegas (~> 0.1.2)
75
+ rexml (3.2.5)
76
+ rspec (3.10.0)
77
+ rspec-core (~> 3.10.0)
78
+ rspec-expectations (~> 3.10.0)
79
+ rspec-mocks (~> 3.10.0)
80
+ rspec-core (3.10.2)
81
+ rspec-support (~> 3.10.0)
82
+ rspec-expectations (3.10.2)
83
+ diff-lcs (>= 1.2.0, < 2.0)
84
+ rspec-support (~> 3.10.0)
85
+ rspec-mocks (3.10.3)
86
+ diff-lcs (>= 1.2.0, < 2.0)
87
+ rspec-support (~> 3.10.0)
88
+ rspec-support (3.10.3)
89
+ rubocop (1.25.0)
90
+ parallel (~> 1.10)
91
+ parser (>= 3.1.0.0)
92
+ rainbow (>= 2.2.2, < 4.0)
93
+ regexp_parser (>= 1.8, < 3.0)
94
+ rexml
95
+ rubocop-ast (>= 1.15.1, < 2.0)
96
+ ruby-progressbar (~> 1.7)
97
+ unicode-display_width (>= 1.4.0, < 3.0)
98
+ rubocop-ast (1.15.1)
99
+ parser (>= 3.0.1.1)
100
+ rubocop-rake (0.6.0)
101
+ rubocop (~> 1.0)
102
+ rubocop-rspec (2.8.0)
103
+ rubocop (~> 1.19)
104
+ ruby-progressbar (1.11.0)
105
+ ruby2_keywords (0.0.5)
106
+ simplecov (0.21.2)
107
+ docile (~> 1.1)
108
+ simplecov-html (~> 0.11)
109
+ simplecov_json_formatter (~> 0.1)
110
+ simplecov-html (0.12.3)
111
+ simplecov_json_formatter (0.1.3)
112
+ sinatra (2.1.0)
113
+ mustermann (~> 1.0)
114
+ rack (~> 2.2)
115
+ rack-protection (= 2.1.0)
116
+ tilt (~> 2.0)
117
+ slack-ruby-client (1.0.0)
118
+ faraday (>= 1.0)
119
+ faraday_middleware
120
+ gli
121
+ hashie
122
+ websocket-driver
123
+ tilt (2.0.10)
124
+ unicode-display_width (2.1.0)
125
+ vegas (0.1.11)
126
+ rack (>= 1.0.0)
127
+ waylon-core (0.1.3)
128
+ addressable (~> 2.8)
129
+ faraday (~> 1.8)
130
+ i18n (~> 1.8)
131
+ json (~> 2.6)
132
+ moneta (~> 1.4)
133
+ puma (~> 5.5)
134
+ resque (~> 2.2)
135
+ webrick (1.7.0)
136
+ websocket-driver (0.7.5)
137
+ websocket-extensions (>= 0.1.0)
138
+ websocket-extensions (0.1.5)
139
+ yard (0.9.27)
140
+ webrick (~> 1.7.0)
141
+
142
+ PLATFORMS
143
+ arm64-darwin-21
144
+
145
+ DEPENDENCIES
146
+ bundler (~> 2.3)
147
+ rake (~> 13.0)
148
+ rspec (~> 3.10)
149
+ rubocop (~> 1.23)
150
+ rubocop-rake (~> 0.6)
151
+ rubocop-rspec (~> 2.6)
152
+ simplecov (~> 0.21)
153
+ waylon-slack!
154
+ yard (~> 0.9, >= 0.9.27)
155
+
156
+ BUNDLED WITH
157
+ 2.3.4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Jonathan Gnagy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Waylon::Slack
2
+
3
+ The Slack _Sense_ for the [Waylon](https://github.com/jgnagy/waylon-core) Bot Framework. This allows Waylon to interact with Slack via the Slack [Web API](https://api.slack.com/web) and [Events API](https://api.slack.com/events) (for sending and receiving messages, respectively).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your bot's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'waylon-slack'
11
+ ```
12
+
13
+ Or, if your bot is itself a gem, add this to your .gemspec:
14
+
15
+ ```ruby
16
+ spec.add_dependency "waylon-slack", "~> 0.1"
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle install
22
+
23
+ Or install it yourself via:
24
+
25
+ $ gem install waylon-slack
26
+
27
+ ## Usage
28
+
29
+ You'll need both your webhook server and your workers to have a line like this:
30
+
31
+ ```ruby
32
+ # right after 'require "waylon"'...
33
+ require "waylon/slack"
34
+ ```
35
+
36
+ That should get it working in your bot. You'll also need to ensure that these environment variables are properly defined based on your bot's [Slack app setup](https://api.slack.com/authentication/basics):
37
+
38
+ ```sh
39
+ SLACK_OAUTH_TOKEN="xoxb-..."
40
+ SLACK_SIGNING_SECRET="..."
41
+ ```
42
+
43
+ ## Development
44
+
45
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
46
+
47
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
48
+
49
+ ## Contributing
50
+
51
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jgnagy/waylon-slack.
52
+
53
+ ## License
54
+
55
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+ require "yard"
7
+ require "resque/tasks"
8
+ require "waylon/core"
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+ RuboCop::RakeTask.new
12
+ YARD::Rake::YardocTask.new do |y|
13
+ y.options = [
14
+ "--markup", "markdown"
15
+ ]
16
+ end
17
+
18
+ task default: %i[spec rubocop yard]
data/bin/console ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "waylon"
6
+ config = Waylon::Config.instance
7
+ config.load_env
8
+
9
+ Waylon::Cache = Moneta.new(:Cookie)
10
+ Waylon::Storage = Moneta.new(:LRUHash)
11
+
12
+ require "waylon/slack"
13
+
14
+ # You can add fixtures and/or initialization code here to make experimenting
15
+ # with your gem easier. You can also use a different console, if you like.
16
+
17
+ # (If you use this, don't forget to add pry to your Gemfile!)
18
+ # require "pry"
19
+ # Pry.start
20
+
21
+ require "irb"
22
+
23
+ include Waylon
24
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waylon
4
+ module Senses
5
+ # The Waylon Sense for interacting with Slack via the Events API
6
+ class Slack < Waylon::Sense
7
+ features %i[blocks private_messages reactions threads]
8
+
9
+ # Provides easy access to the Slack Web Client for interacting with the Slack API
10
+ # @return [Slack::Web::Client]
11
+ def self.client
12
+ @client ||= ::Slack::Web::Client.new
13
+ end
14
+
15
+ # Takes an incoming request from a webhook and converts it to a usable Waylon Message
16
+ # @param request [Hash,Waylon::Message]
17
+ # @return [Waylon::Message]
18
+ def self.message_from_request(request)
19
+ return request if request.is_a?(message_class)
20
+
21
+ if request["type"] == "event_callback" && %w[app_mention message].include?(request.dig("event", "type"))
22
+ # These are typical chat messages
23
+ message_class.new(request["event_id"], request["event"])
24
+ elsif request["type"] == "event_callback"
25
+ log("Support for events of type #{request.dig("event", "type")} not yet implemented")
26
+ end
27
+ end
28
+
29
+ # "At-mention" for Slack.
30
+ # @param user [Waylon::User] The User to mention
31
+ # @return [String]
32
+ def self.mention(user)
33
+ "<@#{user.handle}>"
34
+ end
35
+
36
+ # Provides a simple means to privately reply to the author of a Message
37
+ # @param request [Hash,Waylon::Message]
38
+ # @param text [String] Reply contents
39
+ # @return [void]
40
+ def self.private_reply(request, text)
41
+ message = message_from_request(request)
42
+ message.author.dm(text: text)
43
+ end
44
+
45
+ # Provides a simple means to privately reply to the author of a Message using Blocks
46
+ # @param request [Hash,Waylon::Message]
47
+ # @param blocks [Array] Blocks data to reply with
48
+ # @return [void]
49
+ def self.private_reply_with_blocks(request, blocks)
50
+ message = message_from_request(request)
51
+ message.author.dm(blocks: blocks)
52
+ end
53
+
54
+ # Allows reacting to a request via the Sense's own mechanism
55
+ # @param request [Hash,Waylon::Message]
56
+ # @param reaction [String]
57
+ # @return [void]
58
+ def self.react(request, reaction)
59
+ message = message_from_request(request)
60
+ message.react(reaction)
61
+ end
62
+
63
+ # Reply to a Message in a Channel with some text
64
+ # @param request [Hash,Waylon::Message]
65
+ # @param text [String] Reply contents
66
+ # @return [void]
67
+ def self.reply(request, text)
68
+ message = message_from_request(request)
69
+ message.channel.post(text: text)
70
+ end
71
+
72
+ # Reply to a Message in a Channel with some blocks
73
+ # @param request [Hash,Waylon::Message]
74
+ # @param blocks [Array] Blocks to reply with
75
+ # @return [void]
76
+ def self.reply_with_blocks(request, blocks)
77
+ message = message_from_request(request)
78
+ message.channel.post(blocks: blocks)
79
+ end
80
+
81
+ # Executed by Resque, this is how this Sense determines what to do with an incoming request
82
+ # @param received_web_content [Hash] The parsed web request content from the Webhook
83
+ # @return [void]
84
+ def self.run(received_web_content)
85
+ log("Received request of type #{received_web_content["type"]}", :debug)
86
+ message = message_from_request(received_web_content)
87
+ unless message
88
+ log("Unable to handle request")
89
+ return
90
+ end
91
+
92
+ if message.author == Waylon::Slack::User.whoami
93
+ log("Ignoring my own message...", :debug)
94
+ return
95
+ end
96
+
97
+ log("Responding to message from bot '#{message.author.handle}'") if message.author.bot?
98
+
99
+ route = Waylon::SkillRegistry.route(message) || SkillRegistry.instance.default_route(message)
100
+ enqueue(route, received_web_content)
101
+ end
102
+
103
+ # Reply to a Message in a Thread with some text
104
+ # @param request [Hash,Waylon::Message]
105
+ # @param text [String] Reply contents
106
+ # @return [void]
107
+ def self.threaded_reply(request, text)
108
+ message = message_from_request(request)
109
+ message.channel.post(text: text, thread: message.thread_parent)
110
+ end
111
+
112
+ # Required by the Waylon framework, this provides the Sense's own Message class
113
+ # @return [Class]
114
+ def self.message_class
115
+ Waylon::Slack::Message
116
+ end
117
+
118
+ # Required by the Waylon framework, this provides the Sense's own User class
119
+ # @return [Class]
120
+ def self.user_class
121
+ Waylon::Slack::User
122
+ end
123
+
124
+ # Automatically informs Waylon about this Sense
125
+ SenseRegistry.register(:slack, self)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waylon
4
+ module Slack
5
+ # A representation of Slack channels for Waylon
6
+ class Channel
7
+ attr_reader :id
8
+
9
+ # Allows finding a channel based on its channel name
10
+ # @raise [Slack::Web::Api::Errors::ChannelNotFound] When the channel doesn't exist
11
+ # @return [Channel]
12
+ def self.from_name(name)
13
+ name = name.start_with?("#") ? name : "##{name}"
14
+ raw = sense.client.conversations_info(channel: name)
15
+ new(raw["channel"]["id"], data: raw["channel"])
16
+ end
17
+
18
+ # Provides direct access to the Sense class
19
+ # @return [Class]
20
+ def self.sense
21
+ ::Waylon::Senses::Slack
22
+ end
23
+
24
+ def initialize(id = nil, data: {})
25
+ raise "Must provide ID or details" unless id || !data.empty?
26
+
27
+ @id = id || data["id"]
28
+ # @data should never be accessed directly... always use the wrapper instance method
29
+ @data = data
30
+ end
31
+
32
+ # Is channel archived (meaning no further messages are possible)?
33
+ # @return [Boolean]
34
+ def archived?
35
+ data["is_archived"].dup
36
+ end
37
+
38
+ # Provides lazy, cached access to the Channel's internal details
39
+ # @return [Hash]
40
+ def data
41
+ if !@data || @data.empty?
42
+ # Only cache channel info for 5 min
43
+ sense.cache("channels.#{id}", expires: 300) do
44
+ raw_data = sense.client.conversations_info(channel: id)
45
+ @data = raw_data["channel"]
46
+ end
47
+ else
48
+ @data
49
+ end
50
+ end
51
+
52
+ # Is this the "main" channel for this team?
53
+ # @return [Boolean]
54
+ def general?
55
+ data["is_general"].dup
56
+ end
57
+
58
+ # Lists channel members
59
+ # @return [Array<User>] channel members
60
+ def members
61
+ # Only cache channel member ids for 5 min
62
+ ids = sense.cache("channels.#{id}.member_ids", expires: 300) do
63
+ member_ids = []
64
+ sense.client.conversations_members(channel: id) do |raw|
65
+ member_ids += raw["members"]
66
+ end
67
+ member_ids.sort.uniq
68
+ end
69
+ ids.map { |m| User.new(m) }
70
+ end
71
+
72
+ # Is this bot a member of the channel?
73
+ # @return [Boolean]
74
+ def member?
75
+ data["is_member"]
76
+ end
77
+
78
+ # The proper channel name
79
+ # @return [String]
80
+ def name
81
+ "##{data["name"]}"
82
+ end
83
+
84
+ # Posts a message to a channel
85
+ # @param text [String] Message text or fallback text for blocks
86
+ # @param attachments [Array<Hash>] Old-style message attachments
87
+ # @param blocks [Array<Hash>] New-style block method of sending complex messages
88
+ # @param thread [Integer] The message timestamp for the thread id
89
+ # @return [void]
90
+ def post(text: nil, attachments: nil, blocks: nil, thread: nil)
91
+ options = { channel: id }
92
+ options[:text] = text if text
93
+ options[:attachments] = attachments if attachments
94
+ options[:blocks] = blocks if blocks
95
+ options[:thread_ts] = thread if thread
96
+ sense.client.chat_postMessage(options)
97
+ end
98
+
99
+ # Is this a private channel? (meaning a direct message, NOT private in the Slack sense)
100
+ # @return [Boolean]
101
+ def private?
102
+ data["is_im"].dup
103
+ end
104
+
105
+ # An instance-level helper to access the class-level method
106
+ # @return [Class]
107
+ def sense
108
+ self.class.sense
109
+ end
110
+
111
+ # Provides access to the Channel's topic
112
+ # @return [String]
113
+ def topic
114
+ data.dig("topic", "value").dup
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waylon
4
+ module Slack
5
+ # A representation of Slack messages for Waylon
6
+ class Message
7
+ include Waylon::Message
8
+
9
+ attr_reader :id, :data
10
+
11
+ def initialize(id, data = {})
12
+ @id = id
13
+ @data = data
14
+ end
15
+
16
+ # The User that authored this Message
17
+ # @return [User]
18
+ def author
19
+ User.new(data["user"])
20
+ end
21
+
22
+ # The Channel where this Message was sent
23
+ # @return [Channel]
24
+ def channel
25
+ Channel.new(data["channel"])
26
+ end
27
+
28
+ # Does the message text mention the bot?
29
+ # @return [Boolean]
30
+ def mentions_bot?
31
+ me = User.whoami
32
+ reg = /(,\s+)?\s*@#{me.id},?\s*/
33
+ ::Slack::Messages::Formatting.unescape(data["text"]) =~ reg ? true : false
34
+ end
35
+
36
+ # Is this Message a reply in a thread?
37
+ # @return [Boolean]
38
+ def part_of_thread?
39
+ thread_ts && thread_ts != ts
40
+ end
41
+
42
+ # Is this a private Message / direct Message?
43
+ # @return [Boolean]
44
+ def private_message?
45
+ channel.private?
46
+ end
47
+
48
+ alias private? private_message?
49
+
50
+ # Uses the Sense's Web Client to add a reaction to a Message
51
+ # @param reaction [String,Symbol] The reaction to add (not wrapped in ":")
52
+ # @return [void]
53
+ def react(reaction)
54
+ sense.client.reactions_add(channel: channel.id, name: reaction, timestamp: ts)
55
+ end
56
+
57
+ # Easy access to the Sense class
58
+ # @return [Class]
59
+ def sense
60
+ ::Waylon::Senses::Slack
61
+ end
62
+
63
+ # The unescaped contents of the Message
64
+ # @return [String]
65
+ def text
66
+ me = User.whoami
67
+ reg = /(,\s+)?\s*@#{me.id},?\s*/
68
+ ::Slack::Messages::Formatting.unescape(data["text"]).gsub(reg, "")
69
+ end
70
+
71
+ alias body text
72
+
73
+ # The TS value of the parent of this Message's thread, or its own TS if it is the parent
74
+ # @return [String]
75
+ def thread_parent
76
+ thread_ts || ts
77
+ end
78
+
79
+ # The TS value of the parent of this Message's thread, if it exists
80
+ # @return [String,nil]
81
+ def thread_ts
82
+ data["thread_ts"].dup
83
+ end
84
+
85
+ # Does this Message either directly mention or is it directly to this bot?
86
+ # @return [Boolean]
87
+ def to_bot?
88
+ data["type"] == "app_mention" || private_message?
89
+ end
90
+
91
+ # This Message's own TS value (which should not be used for threading if it itself is a thread reply)
92
+ # @return [String]
93
+ def ts
94
+ data["ts"].dup
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waylon
4
+ module Slack
5
+ # A representation of Slack users for Waylon
6
+ class User
7
+ include Waylon::User
8
+
9
+ # Find a Slack User based on their IM handle
10
+ # @param handle [String]
11
+ # @return [User]
12
+ def self.find_by_handle(handle)
13
+ real_handle = handle.start_with?("@") ? handle : "@#{handle}"
14
+ from_response(sense.client.users_info(user: real_handle))
15
+ end
16
+
17
+ # Find a Slack User based on their email address
18
+ # @param email [String]
19
+ # @return [User]
20
+ # @note Not recommended as it requires an additional scope on the OAuth token
21
+ def self.find_by_email(email)
22
+ from_response(sense.client.users_lookupByEmail(email))
23
+ end
24
+
25
+ # Find a Slack User based on the text provided by Slack from a mention of that User
26
+ # @param mention_string [String]
27
+ # @return [User]
28
+ def self.from_mention(mention_string)
29
+ from_response(sense.client.users_info(user: mention_string[1..]))
30
+ end
31
+
32
+ # Provide a Slask User based on a Web API response
33
+ # @param response [Hash] The response from a Web API request
34
+ # @return [User]
35
+ def self.from_response(response)
36
+ raise "Failed Request" unless response && response["ok"]
37
+
38
+ new(data: response["user"])
39
+ end
40
+
41
+ # Allows easy access to the Sense class
42
+ # @return [Class]
43
+ def self.sense
44
+ ::Waylon::Senses::Slack
45
+ end
46
+
47
+ # A convenient way use the Slack API to figure out the bot's own info
48
+ # @return [User]
49
+ def self.whoami
50
+ response = sense.cache("whoami") { sense.client.auth_test }
51
+ new(response["user_id"])
52
+ end
53
+
54
+ def initialize(id = nil, data: {})
55
+ raise "Must provide ID or details" unless id || !data.empty?
56
+
57
+ @id = id || data["id"]
58
+ @data = data
59
+ end
60
+
61
+ # Provides lazy, cached access to the User's internal details
62
+ # @return [Hash]
63
+ def data
64
+ if !@data || @data.empty?
65
+ sense.cache("users.#{id}") do
66
+ raw_data = sense.client.users_info(user: id)
67
+ @data = raw_data["user"]
68
+ end
69
+ else
70
+ @data
71
+ end
72
+ end
73
+
74
+ # Is this user a bot?
75
+ # @return [Boolean]
76
+ def bot?
77
+ data["is_bot"]
78
+ end
79
+
80
+ # Posts a direct (private) message to a user
81
+ # @param text [String] Message text or fallback text for blocks
82
+ # @param attachments [Array<Hash>] Old-style message attachments
83
+ # @param blocks [Array<Hash>] New-style block method of sending complex messages
84
+ # @param thread [Integer] The message timestamp for the thread id
85
+ # @return [void]
86
+ def dm(text: nil, attachments: nil, blocks: nil, thread: nil)
87
+ options = { channel: id } # Sends a message to the user's ID
88
+ options[:text] = text if text
89
+ options[:attachments] = attachments if attachments
90
+ options[:blocks] = blocks if blocks
91
+ options[:thread_ts] = thread if thread
92
+ sense.client.chat_postMessage(options)
93
+ end
94
+
95
+ # The User's email address
96
+ # @return [String]
97
+ def email
98
+ profile["email"]
99
+ end
100
+
101
+ # The User's username/chat handle
102
+ # @return [String]
103
+ def handle
104
+ bot? ? data["real_name"] : data["name"]
105
+ end
106
+
107
+ # The User's profile information
108
+ # @return [Slack::Messages::Message]
109
+ def profile
110
+ data["profile"]
111
+ end
112
+
113
+ # Easy access to the Sense class
114
+ # @return [Class]
115
+ def sense
116
+ self.class.sense
117
+ end
118
+
119
+ # The User's current Status (comes from cache so can be outdated)
120
+ # @return [String]
121
+ def status
122
+ profile["status_text"]
123
+ end
124
+
125
+ # The User's Slack team ID
126
+ # @return [String]
127
+ def team
128
+ data["team_id"]
129
+ end
130
+
131
+ # The User's time zone
132
+ # @return [String]
133
+ def tz
134
+ data["tz"]
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waylon
4
+ module Slack
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "waylon/core"
4
+ require "slack-ruby-client"
5
+
6
+ ::Slack.configure do |conf|
7
+ conf.token = ENV["SLACK_OAUTH_TOKEN"]
8
+ conf.logger = Waylon::Logger.logger
9
+ end
10
+
11
+ ::Slack::Web::Client.configure do |conf|
12
+ conf.user_agent = "Waylon/#{Waylon::Core::VERSION}"
13
+ conf.logger = Waylon::Logger.logger
14
+ end
15
+
16
+ require_relative "slack/version"
17
+ require_relative "slack/user"
18
+ require_relative "slack/channel"
19
+ require_relative "slack/message"
20
+ require_relative "senses/slack"
21
+ require_relative "webhooks/slack"
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Waylon
4
+ module Webhooks
5
+ # Webhook for the Slack Sense for Waylon
6
+ class Slack < Waylon::Webhook
7
+ before do
8
+ content_type "application/json"
9
+ end
10
+
11
+ post "/" do
12
+ request.body.rewind
13
+ verify(request) unless ENV.fetch("LOCAL_MODE", false)
14
+ if @parsed_body.is_a?(Hash) && @parsed_body[:type] == "url_verification"
15
+ { challenge: @parsed_body[:challenge] }.to_json
16
+ else
17
+ enqueue(@parsed_body)
18
+ { status: :ok }.to_json
19
+ end
20
+ rescue ::Slack::Events::Request::InvalidSignature, ::Slack::Events::Request::TimestampExpired
21
+ halt(403, { error: "Unable to authenticate request" }.to_json)
22
+ rescue StandardError => e
23
+ log("Encountered #{e.message}", :warn)
24
+ halt(422, { error: "Unprocessable entity: #{e.message}" }.to_json)
25
+ end
26
+
27
+ options "/" do
28
+ halt 200
29
+ end
30
+
31
+ # Used to verify incoming Slack requests
32
+ def verify(incoming_request)
33
+ slack_request = ::Slack::Events::Request.new(incoming_request)
34
+ slack_request.verify!
35
+ end
36
+
37
+ # Automatically informs Waylon about this Webhook
38
+ Waylon::WebhookRegistry.register(:slack, self)
39
+ end
40
+ end
41
+ end
data/scripts/test.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ gem install bundler -v '~> 2.2'
4
+ bundle install
5
+ bundle exec rake
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/waylon/slack/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "waylon-slack"
7
+ spec.version = Waylon::Slack::VERSION
8
+ spec.authors = ["Jonathan Gnagy"]
9
+ spec.email = ["jonathan.gnagy@gmail.com"]
10
+
11
+ spec.summary = "Slack Sense for the Waylon Bot Framework"
12
+ spec.description = "Full Slack integration for the Waylon Bot Framework"
13
+ spec.homepage = "https://github.com/jgnagy/waylon-slack"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = "~> 3.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/jgnagy/waylon-slack"
19
+ spec.metadata["changelog_uri"] = "https://github.com/jgnagy/waylon-slack/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "slack-ruby-client", "~> 1.0"
31
+ spec.add_dependency "waylon-core", "~> 0.1"
32
+
33
+ spec.add_development_dependency "bundler", "~> 2.3"
34
+ spec.add_development_dependency "rake", "~> 13.0"
35
+ spec.add_development_dependency "rspec", "~> 3.10"
36
+ spec.add_development_dependency "rubocop", "~> 1.23"
37
+ spec.add_development_dependency "rubocop-rake", "~> 0.6"
38
+ spec.add_development_dependency "rubocop-rspec", "~> 2.6"
39
+ spec.add_development_dependency "simplecov", "~> 0.21"
40
+ spec.add_development_dependency "yard", "~> 0.9", ">= 0.9.27"
41
+
42
+ # For more information and examples about making a new gem, checkout our
43
+ # guide at: https://bundler.io/guides/creating_gem.html
44
+ end
metadata ADDED
@@ -0,0 +1,214 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: waylon-slack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Gnagy
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: slack-ruby-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: waylon-core
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.10'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.23'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.23'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.6'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.6'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2.6'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2.6'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.21'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.21'
139
+ - !ruby/object:Gem::Dependency
140
+ name: yard
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.9'
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: 0.9.27
149
+ type: :development
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - "~>"
154
+ - !ruby/object:Gem::Version
155
+ version: '0.9'
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: 0.9.27
159
+ description: Full Slack integration for the Waylon Bot Framework
160
+ email:
161
+ - jonathan.gnagy@gmail.com
162
+ executables: []
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - ".gitignore"
167
+ - ".roxanne.yml"
168
+ - ".rspec"
169
+ - ".rubocop.yml"
170
+ - ".ruby-version"
171
+ - CHANGELOG.md
172
+ - Gemfile
173
+ - Gemfile.lock
174
+ - LICENSE.txt
175
+ - README.md
176
+ - Rakefile
177
+ - bin/console
178
+ - bin/setup
179
+ - lib/waylon/senses/slack.rb
180
+ - lib/waylon/slack.rb
181
+ - lib/waylon/slack/channel.rb
182
+ - lib/waylon/slack/message.rb
183
+ - lib/waylon/slack/user.rb
184
+ - lib/waylon/slack/version.rb
185
+ - lib/waylon/webhooks/slack.rb
186
+ - scripts/test.sh
187
+ - waylon-slack.gemspec
188
+ homepage: https://github.com/jgnagy/waylon-slack
189
+ licenses:
190
+ - MIT
191
+ metadata:
192
+ homepage_uri: https://github.com/jgnagy/waylon-slack
193
+ source_code_uri: https://github.com/jgnagy/waylon-slack
194
+ changelog_uri: https://github.com/jgnagy/waylon-slack/blob/main/CHANGELOG.md
195
+ post_install_message:
196
+ rdoc_options: []
197
+ require_paths:
198
+ - lib
199
+ required_ruby_version: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - "~>"
202
+ - !ruby/object:Gem::Version
203
+ version: '3.0'
204
+ required_rubygems_version: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ requirements: []
210
+ rubygems_version: 3.3.3
211
+ signing_key:
212
+ specification_version: 4
213
+ summary: Slack Sense for the Waylon Bot Framework
214
+ test_files: []