slack_sender 0.1.0 → 0.1.1

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.
data/lib/slack_sender.rb CHANGED
@@ -26,23 +26,7 @@ module SlackSender
26
26
  # Raised for invalid arguments that should not be retried
27
27
  # (e.g., missing content, invalid blocks, incompatible options)
28
28
  class InvalidArgumentsError < Error; end
29
- end
30
-
31
- require_relative "slack_sender/channel_normalizer"
32
- require_relative "slack_sender/profile"
33
- require_relative "slack_sender/profile_registry"
34
- require_relative "slack_sender/delivery_axn"
35
- require_relative "slack_sender/file_wrapper"
36
- require_relative "slack_sender/multi_file_wrapper"
37
- require_relative "slack_sender/file_uploader"
38
- require_relative "slack_sender/strategy"
39
-
40
- # Register the slack strategy with Axn (before loading Notifier which uses it)
41
- Axn::Strategies.register(:slack, SlackSender::Strategy)
42
-
43
- require_relative "slack_sender/notifier"
44
29
 
45
- module SlackSender
46
30
  class << self
47
31
  def register(name = nil, **config)
48
32
  ProfileRegistry.register(name.presence || :default, config)
@@ -67,5 +51,19 @@ module SlackSender
67
51
  end
68
52
  end
69
53
 
54
+ require_relative "slack_sender/channel_normalizer"
55
+ require_relative "slack_sender/profile"
56
+ require_relative "slack_sender/profile_registry"
57
+ require_relative "slack_sender/delivery_axn"
58
+ require_relative "slack_sender/file_wrapper"
59
+ require_relative "slack_sender/multi_file_wrapper"
60
+ require_relative "slack_sender/file_uploader"
61
+ require_relative "slack_sender/strategy"
62
+
63
+ # Register the slack strategy with Axn (before loading Notifier which uses it)
64
+ Axn::Strategies.register(:slack, SlackSender::Strategy)
65
+
66
+ require_relative "slack_sender/notifier"
67
+
70
68
  # Rails integration (if in Rails context)
71
69
  require_relative "slack_sender/rails/engine" if defined?(Rails) && Rails.const_defined?(:Engine)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slack_sender
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kali Donovan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-19 00:00:00.000000000 Z
11
+ date: 2026-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: axn
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.1.0.pre.alpha.4.1
19
+ version: 0.1.0.pre.alpha.5
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: 0.2.0
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 0.1.0.pre.alpha.4.1
29
+ version: 0.1.0.pre.alpha.5
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.2.0
@@ -36,14 +36,20 @@ dependencies:
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: '2.7'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '4'
40
43
  type: :runtime
41
44
  prerelease: false
42
45
  version_requirements: !ruby/object:Gem::Requirement
43
46
  requirements:
44
47
  - - ">="
45
48
  - !ruby/object:Gem::Version
46
- version: '0'
49
+ version: '2.7'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '4'
47
53
  description: Slack messaging with background dispatch with automatic rate-limit retries.
48
54
  email:
49
55
  - kali@teamshares.com
@@ -51,15 +57,8 @@ executables: []
51
57
  extensions: []
52
58
  extra_rdoc_files: []
53
59
  files:
54
- - ".husky/pre-commit"
55
- - ".lintstagedrc"
56
60
  - CHANGELOG.md
57
61
  - README.md
58
- - Rakefile
59
- - docs/axn_integration.md
60
- - docs/configuration.md
61
- - docs/troubleshooting.md
62
- - docs/usage.md
63
62
  - lib/slack_sender.rb
64
63
  - lib/slack_sender/channel_normalizer.rb
65
64
  - lib/slack_sender/configuration.rb
data/.husky/pre-commit DELETED
@@ -1 +0,0 @@
1
- npx lint-staged
data/.lintstagedrc DELETED
@@ -1 +0,0 @@
1
- {"*.rb":["bundle exec rubocop -A -c .rubocop.yml --force-exclusion"]}
data/Rakefile DELETED
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
- require "rubocop/rake_task"
6
-
7
- RSpec::Core::RakeTask.new(:spec)
8
-
9
- RuboCop::RakeTask.new
10
-
11
- task default: %i[spec rubocop]
12
-
13
- # Require default to pass before release. This relies on the default gem release task
14
- # (from bundler/gem_tasks) depending on "build"; default runs before build, so before push.
15
- Rake::Task["build"].enhance([:default])
@@ -1,168 +0,0 @@
1
- # Axn Integration
2
-
3
- [← Back to README](../README.md)
4
-
5
- SlackSender provides deep integration with [Axn](https://teamshares.github.io/axn/) for building Slack-enabled actions and dedicated notifier classes.
6
-
7
- ## Slack Strategy for Axn Actions
8
-
9
- Add Slack messaging capabilities to any Axn action using the `:slack` strategy:
10
-
11
- ```ruby
12
- class Deployments::Finish
13
- include Axn
14
- use :slack, channel: :deployments # Default channel for all slack() calls
15
-
16
- expects :deployment, type: Deployment
17
-
18
- on_success { slack ":rocket: Deploy finished for `#{deployment.service}`" }
19
- on_failure { slack ":x: Deploy failed for `#{deployment.service}`", channel: :ops_alerts }
20
-
21
- def call
22
- # slack() is async (background job) - recommended for fire-and-forget
23
- slack "Finalizing deploy for `#{deployment.service}`..."
24
-
25
- # slack!() is sync - use when you need the thread_ts
26
- thread_ts = slack! "Starting rollout..."
27
- # ... rollout / status checks / persistence ...
28
- slack "Rollout complete!", thread_ts: thread_ts
29
- end
30
- end
31
- ```
32
-
33
- ### Strategy Configuration
34
-
35
- ```ruby
36
- use :slack, channel: :general # Default channel for all slack() calls
37
- use :slack, channel: :general, profile: :support # Use a specific SlackSender profile
38
- use :slack, channels: [:alerts, :ops] # Default to multiple channels (async only)
39
- use :slack # No default channel (must pass channel: each time)
40
- ```
41
-
42
- ### The `slack(...)` and `slack!(...)` Methods
43
-
44
- The strategy adds two instance methods for sending Slack messages:
45
-
46
- | Method | Delivery | Return Value | Use When |
47
- |--------|----------|--------------|----------|
48
- | `slack(...)` | Async (background job) | `true` or `false` | Default; enables auto-retry for rate limits |
49
- | `slack!(...)` | Sync (immediate) | Thread timestamp or `false` | You need the `thread_ts` return value |
50
-
51
- ```ruby
52
- # Async delivery (recommended) - uses Sidekiq or ActiveJob
53
- slack "Hello world"
54
- slack "Hello", channel: :other_channel
55
-
56
- # Sync delivery - immediate execution, returns thread_ts
57
- thread_ts = slack! "Starting deployment..."
58
- slack! "Deployment finished", thread_ts: thread_ts
59
-
60
- # Full kwargs work with both methods
61
- slack text: "Hello", channel: :ops_alerts, icon_emoji: "robot"
62
- slack! channel: :ops_alerts, blocks: [{ type: "section", text: { type: "mrkdwn", text: "*Bold*" } }]
63
- ```
64
-
65
- **Note:** `slack(...)` requires an async backend to be configured (Sidekiq or ActiveJob). If no async backend is available, it raises `SlackSender::Error` with instructions to either use `slack!(...)` or configure an async backend.
66
-
67
- ---
68
-
69
- ## SlackSender::Notifier Base Class
70
-
71
- For actions whose sole purpose is sending Slack notifications, inherit from `SlackSender::Notifier`. These are built on top of Axn (that's where the `expects` DSL comes from below), so you'll want to [familiarize yourself with that library](https://teamshares.github.io/axn/) before continuing:
72
-
73
- ```ruby
74
- # app/slack_notifiers/deployments/finished.rb
75
- module SlackNotifiers
76
- module Deployments
77
- class Finished < SlackSender::Notifier
78
- expects :deployment_id, type: Integer
79
-
80
- # Post to the deployments channel for production releases
81
- notify do
82
- channel :deployments
83
- only_if { production_release? }
84
- text { ":rocket: *Deploy finished* for `#{deployment.service}` (#{deployment.environment})" }
85
- end
86
-
87
- # Optionally also post in the incident channel if this deploy is related to an incident
88
- notify do
89
- channel :incident_channel_id
90
- only_if { incident_channel_id.present? }
91
- text { ":rocket: *Deploy finished* for `#{deployment.service}` (#{deployment.environment})" }
92
- end
93
-
94
- private
95
-
96
- def production_release? = deployment.environment.to_s == "production"
97
-
98
- # Dynamic channel ID string (e.g., "C123...") sourced from your domain model
99
- def incident_channel_id = deployment.incident_slack_channel_id
100
-
101
- def deployment = @deployment ||= Deployment.find(deployment_id)
102
- end
103
- end
104
- end
105
-
106
- # Call it like any Axn
107
- SlackNotifiers::Deployments::Finished.call(deployment_id: 123)
108
- ```
109
-
110
- ---
111
-
112
- ## The `notify do ... end` DSL
113
-
114
- The `notify` block groups all Slack message configuration together, keeping it visually separated from Axn declarations like `expects`:
115
-
116
- ```ruby
117
- notify do
118
- channel :notifications # Single channel
119
- text { "Hello!" } # Dynamic text (block)
120
- end
121
-
122
- notify do
123
- channels :ops_alerts, :ic # Multiple channels (files uploaded once, shared to all)
124
- only_if { priority == :high } # Conditional send
125
- text :message_text # Text from method
126
- attachments :build_attachments # Attachments from method
127
- end
128
- ```
129
-
130
- ### DSL Options
131
-
132
- | Option | Description |
133
- |--------|-------------|
134
- | `channel :sym` | Single channel (symbol resolved via profile, or method if defined) |
135
- | `channels :a, :b` | Multiple channels |
136
- | `text { ... }` | Text content (block evaluated in instance context) |
137
- | `text :method` | Text from method |
138
- | `text "static"` | Static text |
139
- | `blocks { ... }` | Slack blocks |
140
- | `attachments { ... }` | Slack attachments |
141
- | `icon_emoji :emoji` | Custom emoji |
142
- | `thread_ts :method` | Thread timestamp |
143
- | `files { ... }` | File attachments |
144
- | `only_if { ... }` | Condition (block) — only send if truthy |
145
- | `only_if :method` | Condition (method) — only send if truthy |
146
- | `profile :name` | Use a specific SlackSender profile |
147
-
148
- ### Value Resolution
149
-
150
- For each field, values are resolved in this order:
151
- 1. **Block**: `text { "dynamic #{value}" }` — evaluated in instance context
152
- 2. **Symbol**: `text :my_method` — calls method if it exists, otherwise treated as literal
153
- 3. **Literal**: `text "static"` — used as-is
154
-
155
- ### Required Fields
156
-
157
- - At least one `channel` or `channels`
158
- - At least one payload field (`text`, `blocks`, `attachments`, or `files`)
159
-
160
- ---
161
-
162
- ## Notifier Features
163
-
164
- Since `SlackSender::Notifier` inherits from Axn, you get:
165
- - `expects` / `exposes` for input/output contracts
166
- - Hooks (`before`, `after`, `on_success`, `on_failure`)
167
- - Automatic logging and error handling
168
- - Async execution with `call_async`
@@ -1,252 +0,0 @@
1
- # Configuration
2
-
3
- [← Back to README](../README.md)
4
-
5
- This guide covers global configuration, profile registration, and sandbox mode settings.
6
-
7
- ## Global Configuration
8
-
9
- Configure SlackSender behavior via `SlackSender.configure` (e.g. in a Rails initializer):
10
-
11
- ```ruby
12
- SlackSender.configure do |config|
13
- # Set async backend (auto-detects Sidekiq or ActiveJob if available)
14
- config.async_backend = :sidekiq # or :active_job
15
-
16
- # Set sandbox mode (affects sandbox channel/user_group redirects)
17
- # Defaults to true in non-production, false in production
18
- config.sandbox_mode = !Rails.env.production?
19
-
20
- # Set default sandbox behavior when sandbox_mode is true but profile
21
- # doesn't specify a sandbox.mode or sandbox.channel.replace_with
22
- # Options: :noop (default), :redirect, :passthrough
23
- config.sandbox_default_behavior = :noop
24
-
25
- # Enable/disable SlackSender globally (default: true)
26
- config.enabled = ENV["DISABLE_SLACK"] != "1"
27
-
28
- # Silence archived channel exceptions (default: false)
29
- config.silence_archived_channel_exceptions = false
30
-
31
- # Control autoloading namespace for app/slack_notifiers (default: true)
32
- # When true: app/slack_notifiers/foo.rb -> SlackNotifiers::Foo
33
- # When false: app/slack_notifiers/foo.rb -> Foo (standard Rails behavior)
34
- config.use_slack_notifiers_namespace = true
35
- end
36
- ```
37
-
38
- ### Global Options Reference
39
-
40
- | Option | Type | Default | Description |
41
- |--------|------|---------|-------------|
42
- | `async_backend` | `Symbol` or `nil` | Auto-detected | Backend for async delivery. Supported: `:sidekiq`, `:active_job` |
43
- | `sandbox_mode` | `Boolean` or `nil` | `!Rails.env.production?` if Rails available, else `true` | Whether app is in sandbox mode |
44
- | `sandbox_default_behavior` | `Symbol` | `:noop` | Default behavior when in sandbox mode if profile doesn't specify. Options: `:noop`, `:redirect`, `:passthrough` |
45
- | `enabled` | `Boolean` | `true` | Global enable/disable flag. When `false`, `call` and `call!` return `false` without sending |
46
- | `silence_archived_channel_exceptions` | `Boolean` | `false` | If `true`, silently ignores `IsArchived` errors instead of reporting them |
47
- | `max_async_file_upload_size` | `Integer` or `nil` | `26_214_400` (25 MB) | Max total file size for async uploads. Set to `nil` to disable |
48
- | `use_slack_notifiers_namespace` | `Boolean` | `true` | When `true`, files in `app/slack_notifiers` are autoloaded under the `SlackNotifiers` namespace |
49
-
50
- ---
51
-
52
- ## Profile Registration
53
-
54
- A **profile** represents a Slack workspace configuration. Register profiles with `SlackSender.register`:
55
-
56
- ```ruby
57
- SlackSender.register(
58
- token: ENV['SLACK_BOT_TOKEN'],
59
- default_channel: :ops_alerts,
60
- channels: {
61
- ops_alerts: 'C1111111111',
62
- deployments: 'C2222222222',
63
- reports: 'C3333333333',
64
- },
65
- user_groups: {
66
- engineers: 'S1234567890',
67
- },
68
- sandbox: {
69
- channel: {
70
- replace_with: 'C1234567890',
71
- message_prefix: ':construction: _This message would have been sent to %s in production_'
72
- },
73
- user_group: {
74
- replace_with: 'S_DEV_GROUP'
75
- }
76
- }
77
- )
78
- ```
79
-
80
- ### Profile Options Reference
81
-
82
- | Option | Type | Default | Description |
83
- |--------|------|---------|-------------|
84
- | `token` | `String` or callable | Required | Slack Bot User OAuth Token. Can be a proc/lambda for dynamic fetching |
85
- | `default_channel` | `Symbol`, `String`, or `nil` | `nil` | Default channel when none is specified in `call`/`call!` |
86
- | `channels` | `Hash` | `{}` | Hash mapping symbol keys to channel IDs (e.g., `{ alerts: 'C123' }`) |
87
- | `user_groups` | `Hash` | `{}` | Hash mapping symbol keys to user group IDs (e.g., `{ engineers: 'S123' }`) |
88
- | `slack_client_config` | `Hash` | `{}` | Additional options passed to `Slack::Web::Client` constructor |
89
- | `sandbox` | `Hash` | `{}` | Sandbox mode configuration (see below) |
90
-
91
- ### Dynamic Token
92
-
93
- Use a callable for the token to fetch it dynamically:
94
-
95
- ```ruby
96
- SlackSender.register(
97
- token: -> { SecretsManager.get_slack_token },
98
- channels: { ops_alerts: 'C123' }
99
- )
100
- ```
101
-
102
- The token is memoized after first access.
103
-
104
- ### Multiple Profiles
105
-
106
- Register multiple profiles for different Slack workspaces:
107
-
108
- ```ruby
109
- # Internal engineering workspace (default profile)
110
- SlackSender.register(
111
- token: ENV['SLACK_BOT_TOKEN'],
112
- channels: { ops_alerts: 'C123', deployments: 'C234' }
113
- )
114
-
115
- # Customer support workspace
116
- SlackSender.register(:support,
117
- token: ENV['SUPPORT_SLACK_TOKEN'],
118
- channels: { support_tickets: 'C456' }
119
- )
120
-
121
- # Use specific profile
122
- SlackSender.profile(:support).call(
123
- channel: :support_tickets,
124
- text: "New high-priority ticket received"
125
- )
126
-
127
- # Or use bracket notation
128
- SlackSender[:support].call(channel: :support_tickets, text: "...")
129
-
130
- # Or override default profile with profile parameter
131
- SlackSender.call(profile: :support, channel: :support_tickets, text: "...")
132
- ```
133
-
134
- ---
135
-
136
- ## Sandbox Mode
137
-
138
- When `config.sandbox_mode?` is true (default in non-production), SlackSender applies sandbox behavior based on the profile's `sandbox` configuration.
139
-
140
- ### Sandbox Options Reference
141
-
142
- | Option | Type | Default | Description |
143
- |--------|------|---------|-------------|
144
- | `behavior` | `Symbol` or `nil` | Inferred | Explicit sandbox behavior: `:redirect`, `:noop`, or `:passthrough` |
145
- | `channel.replace_with` | `String` or `nil` | `nil` | Channel ID to redirect all messages when behavior is `:redirect` |
146
- | `channel.message_prefix` | `String` or `nil` | See below | Custom prefix for sandbox channel redirects. Use `%s` placeholder for channel name |
147
- | `user_group.replace_with` | `String` or `nil` | `nil` | User group ID to replace all group mentions when in sandbox mode |
148
-
149
- Default message prefix: `:construction: _This message would have been sent to %s in production_`
150
-
151
- ### Behavior Resolution
152
-
153
- When `config.sandbox_mode?` is true, the effective sandbox behavior is determined by:
154
-
155
- 1. **Explicit `sandbox.behavior`** — if set, use it
156
- 2. **Inferred from `sandbox.channel.replace_with`** — if present, behavior is `:redirect`
157
- 3. **Global default** — `config.sandbox_default_behavior` (defaults to `:noop`)
158
-
159
- | Behavior | Description |
160
- |----------|-------------|
161
- | `:redirect` | Redirect messages to `sandbox.channel.replace_with` (required). Adds message prefix. |
162
- | `:noop` | Don't send anything. Logs what would have been sent. Returns `false`. |
163
- | `:passthrough` | Send to real channel (explicit opt-out of sandbox safety). |
164
-
165
- ### Mode: Redirect
166
-
167
- Redirect all messages to a sandbox channel:
168
-
169
- ```ruby
170
- SlackSender.register(
171
- token: ENV['SLACK_BOT_TOKEN'],
172
- channels: { production_alerts: 'C9999999999' },
173
- sandbox: {
174
- behavior: :redirect, # Optional - inferred when channel.replace_with is set
175
- channel: {
176
- replace_with: 'C1234567890',
177
- message_prefix: ':test_tube: Sandbox redirect from %s'
178
- }
179
- }
180
- )
181
-
182
- # In sandbox mode, this goes to C1234567890 with a prefix
183
- SlackSender.call(channel: :production_alerts, text: "Critical alert")
184
- ```
185
-
186
- ### Mode: Noop (Default)
187
-
188
- Don't send anything, just log what would have been sent:
189
-
190
- ```ruby
191
- SlackSender.register(
192
- token: ENV['SLACK_BOT_TOKEN'],
193
- channels: { alerts: 'C999' },
194
- sandbox: { behavior: :noop }
195
- )
196
-
197
- # In sandbox mode, this logs the message but doesn't send to Slack
198
- SlackSender.call(channel: :alerts, text: "Test message")
199
- # => Logs: "[SANDBOX NOOP] Profile: default | Channel: <#C999> | Text: Test message"
200
- # => Returns false
201
- ```
202
-
203
- ### Mode: Passthrough
204
-
205
- Explicitly opt out of sandbox safety and send to real channels:
206
-
207
- ```ruby
208
- SlackSender.register(
209
- token: ENV['SLACK_BOT_TOKEN'],
210
- channels: { alerts: 'C999' },
211
- sandbox: { behavior: :passthrough }
212
- )
213
-
214
- # In sandbox mode, this still sends to the real channel
215
- SlackSender.call(channel: :alerts, text: "This goes to production!")
216
- ```
217
-
218
- ---
219
-
220
- ## Required Slack Scopes
221
-
222
- Your Slack app needs specific OAuth scopes depending on which features you use. Add these under **OAuth & Permissions** → **Bot Token Scopes** in your [Slack app settings](https://api.slack.com/apps).
223
-
224
- **Minimum scopes for basic messaging:**
225
- - `chat:write`
226
-
227
- **Recommended scopes for full functionality:**
228
-
229
- | Scope | Required For | Notes |
230
- |-------|--------------|-------|
231
- | `chat:write` | All messaging | Required for `chat.postMessage` |
232
- | `chat:write.public` | Public channels | Post to public channels your bot hasn't been added to |
233
- | `files:write` | File uploads | Required for `files.getUploadURLExternal` and `files.completeUploadExternal` |
234
- | `files:read` | File metadata | Required if you need thread timestamps from file uploads |
235
-
236
- After adding scopes, reinstall the app to your workspace to apply the changes.
237
-
238
- ---
239
-
240
- ## Exception Notifications
241
-
242
- Exception notifications to error tracking services (e.g., Honeybadger) are handled via Axn's `on_exception` handler:
243
-
244
- ```ruby
245
- Axn.configure do |c|
246
- c.on_exception = proc do |e, action:, context:|
247
- Honeybadger.notify(e, context: { axn_context: context })
248
- end
249
- end
250
- ```
251
-
252
- See [Axn configuration documentation](https://teamshares.github.io/axn/reference/configuration#on_exception) for details.