waylon-slack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []