neeto_ws_pusher 1.0.3

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: cdbf8416e76dd84e71fa296bc9a8bc5a9e26020fb06a5adeb9c1472bc726551e
4
+ data.tar.gz: 3d01eb7c7881386c2688d68c239cc42330f244c9ad48380cb353a713bbd560cf
5
+ SHA512:
6
+ metadata.gz: 50d4399e5c280ab98998759509a1d8203fcec902cd41b5a3e74dede7949d7444cc0338f1a6baf406109d8abcadd0b9a884d30043c715caff5afd4eb97afe3e2c
7
+ data.tar.gz: 62dc6c26223b7cd9eda7724126082fc16bcb5bea81bfddfbb478facd7186adefb724b4dce59e1023c8aa1e67334e4bab89a1ff1771152dad5d76b7abecf9dc3a
data/CHANGELOG.md ADDED
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.3] - 2025-11-17
9
+
10
+ ### Added
11
+ - Added support for Rails 8.1+
12
+
13
+ ## [1.0.0] - 2025-05-02
14
+
15
+ ### Changed
16
+ - **BREAKING**: Changed API endpoint path from `/neeto-ws/api/v1/ws_auth` to `/ws/auth`
17
+ - **BREAKING**: Simplified controller structure
18
+ - **BREAKING**: Removed nested API/v1 namespacing
19
+ - **BREAKING**: Removed unused concerns (WebSocketAuthentication and ErrorHandling)
20
+
21
+ ## [0.2.0] - 2025-05-02
22
+
23
+ ### Added
24
+ - Added support for Rails 7+
25
+
26
+ ## [0.1.0] - 2025-05-02
27
+
28
+ ### Added
29
+ - Initial release
30
+ - Core client for sending messages to SQS
31
+ - Command-line interface
32
+ - Configuration system for global settings
33
+ - Comprehensive documentation
34
+
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
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 all
13
+ 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 THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # NeetoWsPusher
2
+
3
+ This gem acts as a client for [neeto-ws-server](https://github.com/bigbinary/neeto-ws-server) to enable real-time communication in your applications.
4
+
5
+ ## How It Works
6
+
7
+ 1. Your application uses this gem to send messages to specific users
8
+ 2. Messages are sent to an AWS SQS queue with user_id and app_name
9
+ 3. The neeto-ws-server listens to the queue and retrieves messages
10
+ 4. The server uses user_id and app_name to find the matching connected clients
11
+ 5. Messages are forwarded only to the intended clients
12
+ 6. Clients receive real-time updates in their browser or mobile app
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'neeto_ws_pusher'
20
+ ```
21
+
22
+ And then execute:
23
+ ```bash
24
+ bundle install
25
+ ```
26
+
27
+ If you're using Rails, run the generator:
28
+
29
+ ```bash
30
+ rails generate neeto_ws_pusher:install
31
+ ```
32
+
33
+ This will:
34
+ - Create an initializer at `config/initializers/neeto_ws_pusher.rb`
35
+ - Mount routes at `/ws` with an authentication endpoint
36
+ - Display instructions for JavaScript client setup
37
+
38
+ ## Configuration
39
+
40
+ ```ruby
41
+ # config/initializers/neeto_ws_pusher.rb
42
+ NeetoWsPusher.configure do |config|
43
+ # AWS Configuration (REQUIRED)
44
+ config.queue_url = ENV['AWS_SQS_URL']
45
+ config.region = ENV['AWS_REGION'] || 'us-east-1'
46
+ config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
47
+ config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
48
+
49
+ # Default Application Name
50
+ # This identifier is used when sending messages
51
+ config.default_app_name = 'your-application-name'
52
+
53
+ # Logging Configuration
54
+ # Set to false to suppress NeetoWsPusher log messages
55
+ config.logging_enabled = true
56
+ end
57
+ ```
58
+
59
+ ## Authentication
60
+
61
+ This gem automatically mounts an authentication endpoint at `/ws/auth` in your Rails application. This endpoint authenticates WebSocket connections:
62
+
63
+ ### How Authentication Works
64
+
65
+ 1. Client reads your authentication cookie (typically a Devise cookie) from the browser
66
+ 2. Client connects to neeto-ws-server and sends the cookie name and value
67
+ 3. The server makes a POST request to your `/ws/auth` endpoint with the cookie
68
+ 4. The `/ws/auth` endpoint validates the cookie using your app's authentication system
69
+ 5. If successful, it returns `user_id` and `app_name`
70
+ 6. The server establishes the WebSocket connection and associates it with this user_id + app_name
71
+ 7. All future messages sent to this user_id + app_name will be delivered to this connection
72
+
73
+ The authentication endpoint requires:
74
+ - `current_user` to be available (typically from Devise)
75
+ - `authenticate_user!` method to verify authentication
76
+
77
+ This cookie-based approach ensures that only authenticated users can establish WebSocket connections.
78
+
79
+ ## Usage
80
+
81
+ ### Direct Ruby Usage
82
+
83
+ ```ruby
84
+ # Create a client instance
85
+ client = NeetoWsPusher.new
86
+
87
+ # Send a message to a specific channel
88
+ client.send_message(
89
+ { event: 'notification', content: 'Hello, World!' },
90
+ channel: 'notifications'
91
+ )
92
+
93
+ # Send a message to a specific user
94
+ client.send_message(
95
+ { event: 'notification', content: 'Hello, World!' },
96
+ user_id: 'user-123'
97
+ )
98
+
99
+ # Send a message to a specific user in a channel
100
+ client.send_message(
101
+ { event: 'notification', content: 'Hello, World!' },
102
+ user_id: 'user-123',
103
+ channel: 'notifications'
104
+ )
105
+ ```
106
+
107
+ ### Rails Integration
108
+
109
+ The gem adds a `send_ws_message` method to User models. The generator will attempt to add this automatically, but if needed, you can add it manually:
110
+
111
+ ```ruby
112
+ # app/models/user.rb
113
+ class User < ApplicationRecord
114
+ include NeetoWsPusher::UserExtension
115
+
116
+ # rest of your model...
117
+ end
118
+ ```
119
+
120
+ Once included, you can use it like this:
121
+
122
+ ```ruby
123
+ # Send a message to a user (uses current_user.id automatically)
124
+ current_user.send_ws_message(
125
+ { event: 'notification', content: 'You have a new message!' }
126
+ )
127
+
128
+ # Send a message to a specific channel
129
+ current_user.send_ws_message(
130
+ { event: 'chat_message', content: 'New chat message' },
131
+ channel: 'chat-room-1'
132
+ )
133
+ ```
134
+
135
+ ## JavaScript Client
136
+
137
+ ### 1. Add the Script to Your Page
138
+
139
+ ```html
140
+ <!-- Include from CDN -->
141
+ <script src="https://cdn.example.com/neeto_ws_pusher.js"></script>
142
+
143
+ <!-- Or from your local servers -->
144
+ <script src="/assets/neeto_ws_pusher.js"></script>
145
+ ```
146
+
147
+ ### 2. Initialize the Client
148
+
149
+ ```javascript
150
+ // Initialize with WebSocket server URL
151
+ const client = new NeetoWsPusher({
152
+ // Required
153
+ wsUrl: 'wss://your-websocket-server.com',
154
+
155
+ // Optional with defaults
156
+ authCookieName: '_neeto_ws_auth', // Name of auth cookie
157
+ baseUrl: window.location.origin, // Base URL for auth endpoint
158
+ autoConnect: true, // Connect automatically
159
+ debug: false, // Show debug logs
160
+
161
+ // Reconnection settings
162
+ reconnect: {
163
+ enabled: true, // Auto-reconnect on disconnect
164
+ maxAttempts: 5, // Maximum reconnection attempts
165
+ delayFactor: 1.5, // Exponential backoff factor
166
+ maxDelay: 30000, // Maximum delay (30 seconds)
167
+ initialDelay: 1000 // First retry after 1 second
168
+ }
169
+ });
170
+
171
+ // Listen for messages
172
+ client.on('message', ({ data }) => {
173
+ console.log('Received message:', data);
174
+
175
+ // Handle message based on event type
176
+ if (data.event === 'notification') {
177
+ showNotification(data.content);
178
+ }
179
+ });
180
+
181
+ // Connection status events
182
+ client.on('connect', () => console.log('Connected!'));
183
+ client.on('disconnect', ({ code }) => console.log('Disconnected!', code));
184
+ client.on('error', ({ event }) => console.error('WebSocket error:', event));
185
+ ```
186
+
187
+ ## CLI Usage
188
+
189
+ ```bash
190
+ # Set environment variables for AWS credentials
191
+ export AWS_ACCESS_KEY_ID=your-key
192
+ export AWS_SECRET_ACCESS_KEY=your-secret
193
+ export AWS_REGION=us-east-1
194
+ export AWS_SQS_URL=https://sqs.region.amazonaws.com/account/queue
195
+
196
+ neeto_ws_pusher --user-id user-123 \
197
+ --app-name my-app \
198
+ --channel notifications \
199
+ --message '{"event":"notification","content":"Hello!"}'
200
+ ```
201
+
202
+ ### Options
203
+
204
+ - `--user-id`: Target user ID (required)
205
+ - `--app-name`: Application name
206
+ - `--channel`: Channel name
207
+ - `--message`: JSON message payload (required)
208
+ - `--queue-url`, `--region`, `--access-key-id`, `--secret-access-key`: AWS options
209
+
210
+ ## License
211
+
212
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,241 @@
1
+ /**
2
+ * NeetoWsPusher - WebSocket client for neeto-ws-server
3
+ */
4
+ class NeetoWsPusher {
5
+ constructor(config = {}) {
6
+ this.config = {
7
+ wsUrl: config.wsUrl,
8
+ authCookieName: config.authCookieName || '_neeto_ws_auth',
9
+ channel: config.channel,
10
+ debug: config.debug || false,
11
+ autoConnect: config.autoConnect !== false,
12
+ reconnect: {
13
+ enabled: config.reconnect?.enabled !== false,
14
+ maxAttempts: config.reconnect?.maxAttempts || 5,
15
+ delayFactor: config.reconnect?.delayFactor || 1.5,
16
+ maxDelay: config.reconnect?.maxDelay || 30000,
17
+ initialDelay: config.reconnect?.initialDelay || 1000
18
+ }
19
+ };
20
+
21
+ this.socket = null;
22
+ this.connected = false;
23
+ this.reconnectAttempts = 0;
24
+ this.reconnectTimeout = null;
25
+ this.skipReconnect = false;
26
+ this.eventHandlers = {
27
+ connect: [],
28
+ disconnect: [],
29
+ message: [],
30
+ error: []
31
+ };
32
+
33
+ if (this.config.autoConnect) {
34
+ this.connect();
35
+ }
36
+ }
37
+
38
+ debug(...args) {
39
+ if (this.config.debug) {
40
+ console.log('[NeetoWsPusher]', ...args);
41
+ }
42
+ }
43
+
44
+ getMyCookie(cookieName) {
45
+ return fetch(`/ws/expose_cookie`, {
46
+ method: 'POST',
47
+ credentials: 'include',
48
+ headers: {
49
+ 'Content-Type': 'application/json'
50
+ },
51
+ body: JSON.stringify({
52
+ name: cookieName,
53
+ channel: this.config.channel
54
+ })
55
+ })
56
+ .then(response => response.json())
57
+ .then(data => {
58
+ this.debug('Cookie value:', data);
59
+ return data.value;
60
+ });
61
+ }
62
+
63
+ connect() {
64
+ return new Promise((resolve, reject) => {
65
+ if (this.socket) {
66
+ this.socket.close();
67
+ }
68
+
69
+ this.getMyCookie(this.config.authCookieName)
70
+ .then(authCookieData => {
71
+ if (!authCookieData) {
72
+ const error = new Error('Authentication cookie not found');
73
+ this._trigger('error', { event: error });
74
+ reject(error);
75
+ return;
76
+ }
77
+
78
+ try {
79
+ const wsUrl = new URL(this.config.wsUrl);
80
+ wsUrl.searchParams.append('auth_cookie_name', this.config.authCookieName);
81
+ wsUrl.searchParams.append('auth_cookie_data', authCookieData);
82
+ wsUrl.searchParams.append('base_url', window.location.origin);
83
+ wsUrl.searchParams.append('channel', this.config.channel);
84
+
85
+ this.socket = new WebSocket(wsUrl.toString());
86
+
87
+ this.socket.addEventListener('open', (event) => {
88
+ this.debug('Connected to channel:', this.config.channel);
89
+ this.connected = true;
90
+ this.reconnectAttempts = 0;
91
+ this._trigger('connect', { event, channel: this.config.channel });
92
+ resolve(event);
93
+ });
94
+
95
+ this.socket.addEventListener('message', (event) => {
96
+ let parsedData;
97
+ try {
98
+ parsedData = JSON.parse(event.data);
99
+ } catch (e) {
100
+ parsedData = event.data;
101
+ }
102
+
103
+ this.debug('Message received:', parsedData);
104
+ this._trigger('message', { data: parsedData, original: event });
105
+ });
106
+
107
+ this.socket.addEventListener('close', (event) => {
108
+ this.debug('Disconnected from channel:', this.config.channel);
109
+ this.connected = false;
110
+ this._trigger('disconnect', {
111
+ code: event.code,
112
+ reason: event.reason,
113
+ event,
114
+ channel: this.config.channel
115
+ });
116
+
117
+ if (this.config.reconnect.enabled && !this.reconnectTimeout && !this.skipReconnect) {
118
+ this.reconnect();
119
+ }
120
+
121
+ if (this.reconnectAttempts === 0) {
122
+ reject(new Error(`Connection closed: ${event.code}`));
123
+ }
124
+ this.skipReconnect = false;
125
+ });
126
+
127
+ this.socket.addEventListener('error', (event) => {
128
+ this.debug('Error:', event);
129
+ this._trigger('error', { event, channel: this.config.channel });
130
+
131
+ if (this.reconnectAttempts === 0) {
132
+ reject(new Error('WebSocket connection error'));
133
+ }
134
+ });
135
+ } catch (error) {
136
+ this._trigger('error', { event: error });
137
+ reject(error);
138
+ }
139
+ })
140
+ .catch(error => {
141
+ this._trigger('error', { event: error });
142
+ reject(error);
143
+ });
144
+ });
145
+ }
146
+
147
+ changeChannel(newChannel) {
148
+ this.debug('Changing channel from', this.config.channel, 'to', newChannel);
149
+ this.config.channel = newChannel;
150
+ return this.connect();
151
+ }
152
+
153
+ disconnect() {
154
+ // Prevent reconnection when disconnecting manually
155
+ this.skipReconnect = true;
156
+
157
+ if (this.socket) {
158
+ this.socket.close();
159
+ }
160
+ if (this.reconnectTimeout) {
161
+ clearTimeout(this.reconnectTimeout);
162
+ this.reconnectTimeout = null;
163
+ }
164
+ }
165
+
166
+ isConnected() {
167
+ return this.connected;
168
+ }
169
+
170
+ getChannel() {
171
+ return this.config.channel;
172
+ }
173
+
174
+ reconnect() {
175
+ if (this.reconnectTimeout) {
176
+ clearTimeout(this.reconnectTimeout);
177
+ this.reconnectTimeout = null;
178
+ }
179
+
180
+ if (this.reconnectAttempts >= this.config.reconnect.maxAttempts) {
181
+ this.debug(`Max reconnect attempts (${this.config.reconnect.maxAttempts}) reached`);
182
+ return;
183
+ }
184
+
185
+ this.reconnectAttempts++;
186
+
187
+ const delay = Math.min(
188
+ this.config.reconnect.initialDelay * Math.pow(this.config.reconnect.delayFactor, this.reconnectAttempts - 1),
189
+ this.config.reconnect.maxDelay
190
+ );
191
+
192
+ this.debug(`Reconnecting (attempt ${this.reconnectAttempts}/${this.config.reconnect.maxAttempts}) in ${delay}ms`);
193
+
194
+ this.reconnectTimeout = setTimeout(() => {
195
+ this.reconnectTimeout = null;
196
+ this.connect().catch(error => {
197
+ this.debug('Reconnect failed:', error);
198
+ });
199
+ }, delay);
200
+ }
201
+
202
+ on(event, callback) {
203
+ if (!this.eventHandlers[event]) {
204
+ throw new Error(`Unknown event: ${event}`);
205
+ }
206
+
207
+ this.eventHandlers[event].push(callback);
208
+
209
+ return () => {
210
+ this.off(event, callback);
211
+ };
212
+ }
213
+
214
+ off(event, callback) {
215
+ if (!this.eventHandlers[event]) {
216
+ return;
217
+ }
218
+
219
+ const index = this.eventHandlers[event].indexOf(callback);
220
+ if (index !== -1) {
221
+ this.eventHandlers[event].splice(index, 1);
222
+ }
223
+ }
224
+
225
+ _trigger(event, data = {}) {
226
+ if (!this.eventHandlers[event]) {
227
+ return;
228
+ }
229
+
230
+ for (const handler of this.eventHandlers[event]) {
231
+ try {
232
+ handler(data);
233
+ } catch (error) {
234
+ console.error(`Error in ${event} handler:`, error);
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ // Make globally available
241
+ window.NeetoWsPusher = NeetoWsPusher;
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoWsPusher
4
+ class CookiesController < ApplicationController
5
+ skip_before_action :verify_authenticity_token
6
+
7
+ def create
8
+ name = params[:name]
9
+ value = request.cookies[name]
10
+ render json: { value: value }.to_json
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoWsPusher
4
+ class WsAuthController < Api::V1::BaseController
5
+ skip_before_action :verify_authenticity_token, raise: false
6
+ skip_before_action :authenticate_user_using_x_auth_token, raise: false
7
+
8
+ before_action :authenticate_user!
9
+
10
+ def create
11
+ # Extract channel from params, default to 'default' if not provided
12
+ channel = params[:channel] || "default"
13
+
14
+ render json: {
15
+ "app_name": NeetoWsPusher.configuration.default_app_name,
16
+ "user_id": current_user.id,
17
+ "channel": channel
18
+ }, status: :ok
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'neeto_ws_pusher'
4
+ require 'optparse'
5
+ require 'json'
6
+
7
+ # Parse command line options
8
+ options = {
9
+ user_id: nil,
10
+ app_name: nil,
11
+ message: nil,
12
+ queue_url: ENV['AWS_SQS_URL'],
13
+ region: ENV['AWS_REGION'] || 'us-east-1',
14
+ aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
15
+ aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
16
+ verbose: false
17
+ }
18
+
19
+ # Parse command line arguments
20
+ parser = OptionParser.new do |opts|
21
+ opts.banner = "Usage: neeto_ws_pusher [options]"
22
+
23
+ opts.on("-u", "--user-id USER_ID", "User ID (required)") do |user_id|
24
+ options[:user_id] = user_id
25
+ end
26
+
27
+ opts.on("-a", "--app-name APP_NAME", "Application name (default: from configuration)") do |app_name|
28
+ options[:app_name] = app_name
29
+ end
30
+
31
+ opts.on("-m", "--message MESSAGE", "JSON message to send (required)") do |message|
32
+ options[:message] = message
33
+ end
34
+
35
+ opts.on("-c", "--channel CHANNEL", "Channel name (optional)") do |channel|
36
+ options[:channel] = channel
37
+ end
38
+
39
+ opts.on("-q", "--queue-url URL", "SQS queue URL (default: ENV['AWS_SQS_URL'])") do |url|
40
+ options[:queue_url] = url
41
+ end
42
+
43
+ opts.on("-r", "--region REGION", "AWS region (default: ENV['AWS_REGION'] or 'us-east-1')") do |region|
44
+ options[:region] = region
45
+ end
46
+
47
+ opts.on("--access-key-id KEY", "AWS access key ID (default: ENV['AWS_ACCESS_KEY_ID'])") do |key|
48
+ options[:aws_access_key_id] = key
49
+ end
50
+
51
+ opts.on("--secret-access-key SECRET", "AWS secret access key (default: ENV['AWS_SECRET_ACCESS_KEY'])") do |secret|
52
+ options[:aws_secret_access_key] = secret
53
+ end
54
+
55
+ opts.on("-v", "--verbose", "Enable verbose logging") do
56
+ options[:verbose] = true
57
+ end
58
+
59
+ opts.on("-h", "--help", "Display this help message") do
60
+ puts opts
61
+ exit
62
+ end
63
+ end
64
+
65
+ begin
66
+ # Parse options
67
+ parser.parse!
68
+
69
+ # Validate required options
70
+ missing = []
71
+ missing << "user_id" unless options[:user_id]
72
+ missing << "message" unless options[:message]
73
+ missing << "queue_url or AWS_SQS_URL" unless options[:queue_url]
74
+ missing << "access_key_id or AWS_ACCESS_KEY_ID" unless options[:aws_access_key_id]
75
+ missing << "secret_access_key or AWS_SECRET_ACCESS_KEY" unless options[:aws_secret_access_key]
76
+
77
+ if !missing.empty?
78
+ puts "Error: Missing required options: #{missing.join(', ')}"
79
+ puts parser
80
+ exit 1
81
+ end
82
+
83
+ # Parse message if it's a JSON string
84
+ if options[:message].is_a?(String) && options[:message].strip.start_with?('{')
85
+ begin
86
+ options[:message] = JSON.parse(options[:message])
87
+ rescue JSON::ParserError => e
88
+ if options[:verbose]
89
+ puts "Warning: Could not parse message as JSON, using as raw string: #{e.message}"
90
+ end
91
+ end
92
+ end
93
+
94
+ # Initialize client with options
95
+ client = NeetoWsPusher.new(
96
+ queue_url: options[:queue_url],
97
+ region: options[:region],
98
+ aws_access_key_id: options[:aws_access_key_id],
99
+ aws_secret_access_key: options[:aws_secret_access_key],
100
+ verbose: options[:verbose]
101
+ )
102
+
103
+ # Send message
104
+ message_id = client.send_message(
105
+ user_id: options[:user_id],
106
+ app_name: options[:app_name],
107
+ message: options[:message],
108
+ channel: options[:channel]
109
+ )
110
+
111
+ # Report success
112
+ puts "\nMessage successfully sent to queue:"
113
+ puts "- Message ID: #{message_id}"
114
+ puts "- User ID: #{options[:user_id]}"
115
+ puts "- App Name: #{options[:app_name] || NeetoWsPusher.configuration.default_app_name}"
116
+ puts "- Channel: #{options[:channel] || 'none'}"
117
+ puts "- Message: #{options[:message].is_a?(Hash) ? JSON.pretty_generate(options[:message]) : options[:message]}"
118
+ puts "- Queue URL: #{options[:queue_url]}"
119
+
120
+ exit 0
121
+ rescue OptionParser::InvalidOption => e
122
+ puts "Error: #{e.message}"
123
+ puts parser
124
+ exit 1
125
+ rescue StandardError => e
126
+ puts "Error: #{e.message}"
127
+ puts "For help, run: neeto_ws_pusher --help"
128
+ exit 1
129
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ NeetoWsPusher::Engine.routes.draw do
4
+ resources :ws_auth, only: [:create]
5
+ resources :cookies, only: [:create], path: "expose_cookie"
6
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoWsPusher
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Installs NeetoWsPusher configuration files"
9
+
10
+ def create_initializer
11
+ template "initializer.rb", "config/initializers/neeto_ws_pusher.rb"
12
+ end
13
+
14
+ def mount_routes
15
+ # Use as: nil to prevent route helper name collision with the gem's name
16
+ route 'mount NeetoWsPusher::Engine => "/ws", as: nil'
17
+ end
18
+
19
+ def show_post_install_message
20
+ say "\n"
21
+ say "===============================================================================", :green
22
+ say "NeetoWsPusher was successfully installed! Next steps:", :green
23
+ say "===============================================================================", :green
24
+ say "\n"
25
+ say "1. Configure AWS credentials in config/initializers/neeto_ws_pusher.rb", :yellow
26
+ say " - Set queue_url, region, aws_access_key_id, and aws_secret_access_key"
27
+ say " - Recommended: use environment variables for these values"
28
+ say "\n"
29
+ say "2. Add the User model extension (optional):", :yellow
30
+ say " Add this line to your User model:", :yellow
31
+ say " include NeetoWsPusher::UserExtension", :blue
32
+ say "\n"
33
+ say " Example:", :yellow
34
+ say " # app/models/user.rb", :blue
35
+ say " class User < ApplicationRecord", :blue
36
+ say " include NeetoWsPusher::UserExtension", :blue
37
+ say " # ...", :blue
38
+ say " end", :blue
39
+ say "\n"
40
+ say "3. Add the JavaScript client to your pages:", :yellow
41
+ say " # From CDN", :blue
42
+ say " <script src=\"https://cdn.example.com/neeto_ws_pusher.js\"></script>", :blue
43
+ say "\n"
44
+ say " # Or from your local assets", :blue
45
+ say " <script src=\"/assets/neeto_ws_pusher.js\"></script>", :blue
46
+ say "\n"
47
+ say "4. Initialize the client:", :yellow
48
+ say " const client = new NeetoWsPusher({", :blue
49
+ say " wsUrl: 'wss://your-websocket-server.com',", :blue
50
+ say " authCookieName: '_your_auth_cookie_name'", :blue
51
+ say " });", :blue
52
+ say "\n"
53
+ say " client.on('message', ({ data }) => {", :blue
54
+ say " console.log('Message received:', data);", :blue
55
+ say " });", :blue
56
+ say "\n"
57
+ say "For more information, see: https://github.com/bigbinary/neeto-ws-pusher#readme", :green
58
+ say "\n"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+ # NeetoWsPusher Configuration
2
+ # IMPORTANT: AWS credentials MUST be set in environment variables.
3
+ # Make sure AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION are set.
4
+ NeetoWsPusher.configure do |config|
5
+ #=======================================================================
6
+ # AWS SQS Configuration (REQUIRED)
7
+ #=======================================================================
8
+
9
+ # SQS Queue URL - REQUIRED
10
+ # This is the URL of your AWS SQS queue that will be used to send WebSocket messages
11
+ # Format: https://sqs.[region].amazonaws.com/[account-id]/[queue-name]
12
+ # Should be set via the AWS_SQS_URL environment variable
13
+ config.queue_url = ENV['AWS_SQS_URL'] # REQUIRED
14
+
15
+ # AWS Region (REQUIRED)
16
+ # The region where your SQS queue is located
17
+ # Should be set via the AWS_REGION environment variable
18
+ config.region = ENV['AWS_REGION'] || 'us-east-1'
19
+
20
+ # AWS Credentials (REQUIRED)
21
+ # You MUST set AWS credentials using environment variables:
22
+ config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID'] # REQUIRED
23
+ config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY'] # REQUIRED
24
+
25
+ #=======================================================================
26
+ # Application Configuration
27
+ #=======================================================================
28
+
29
+ # Default Application Name
30
+ # This identifier is used:
31
+ # 1. In the WebSocket authentication endpoint response
32
+ # 2. As the default app_name when sending messages via User#send_ws_message
33
+ #
34
+ # You should change this to a unique identifier for your application
35
+ config.default_app_name = 'your-application-name'
36
+
37
+ #=======================================================================
38
+ # Logging Configuration
39
+ #=======================================================================
40
+
41
+ # Enable/Disable gem logging
42
+ # Set to false to suppress NeetoWsPusher log messages (e.g., User model extension notifications)
43
+ # Default: true (enabled for backward compatibility)
44
+ config.logging_enabled = true
45
+
46
+ # NOTE: The gem automatically mounts an authentication endpoint at /ws/auth
47
+ # This endpoint integrates with your application's authentication system
48
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws-sdk-sqs"
4
+ require "json"
5
+ require "logger"
6
+
7
+ module NeetoWsPusher
8
+ # Client class for sending messages to WebSocket server via SQS
9
+ class Client
10
+ attr_accessor :logger, :sqs_client, :queue_url
11
+
12
+ # Initialize a new client
13
+ #
14
+ # @param options [Hash] Options for the client
15
+ # @option options [String] :queue_url SQS queue URL (required unless configured globally)
16
+ # @option options [String] :region AWS region (defaults to 'us-east-1')
17
+ # @option options [Logger] :logger Custom logger (defaults to STDOUT)
18
+ # @option options [Boolean] :verbose Enable verbose logging
19
+ def initialize(options = {})
20
+ @options = options
21
+ setup_logger
22
+ setup_sqs_client
23
+ end
24
+
25
+ # Send a message to a specific user in an application
26
+ #
27
+ # @param message [Hash, String] Message payload (will be converted to JSON if Hash)
28
+ # @param user_id [String] Target user ID (optional, but either user_id or channel must be provided)
29
+ # @param channel [String] Channel name (optional, but either user_id or channel must be provided)
30
+ # @return [String] SQS message ID if successful
31
+ # @raise [StandardError] If required parameters are missing or if SQS returns an error
32
+ # @example Send a message to a channel
33
+ # client.send_message({ event: 'notification', content: 'Hello!' }, channel: 'notifications')
34
+ # @example Send a message to a specific user
35
+ # client.send_message({ event: 'notification', content: 'Hello!' }, user_id: 'user-123')
36
+ # @example Send a message to a specific user in a channel
37
+ # client.send_message({ event: 'notification', content: 'Hello!' }, user_id: 'user-123', channel: 'notifications')
38
+ def send_message(message, user_id: nil, channel: nil)
39
+ validate_parameters(message, user_id, channel)
40
+
41
+ message_payload = {
42
+ app_name: NeetoWsPusher.configuration.default_app_name,
43
+ message: message.is_a?(String) ? parse_json(message) : message
44
+ }
45
+
46
+ message_payload[:user_id] = user_id if user_id
47
+ message_payload[:channel] = channel if channel
48
+
49
+ @logger.debug("Message payload: #{message_payload}")
50
+ @logger.info("Sending message to SQS queue")
51
+
52
+ response = @sqs_client.send_message(
53
+ {
54
+ queue_url: @queue_url,
55
+ message_body: message_payload.to_json
56
+ })
57
+
58
+ @logger.info("Message successfully queued with ID: #{response.message_id}")
59
+
60
+ response.message_id
61
+ end
62
+
63
+ private
64
+
65
+ def setup_logger
66
+ @logger = @options[:logger] || Logger.new($stdout)
67
+ @logger.formatter = proc { |severity, _, _, msg| "[NeetoWsPusher][#{severity}]: #{msg}\n" }
68
+ @logger.level = if NeetoWsPusher.configuration.logging_enabled
69
+ @options[:verbose] ? Logger::DEBUG : Logger::INFO
70
+ else
71
+ Logger::FATAL
72
+ end
73
+ end
74
+
75
+ def setup_sqs_client
76
+ @queue_url = @options[:queue_url] || NeetoWsPusher.configuration.queue_url
77
+ region = @options[:region] || NeetoWsPusher.configuration.region
78
+
79
+ raise Error, "SQS queue URL is required" unless @queue_url
80
+
81
+ # Build AWS SDK configuration
82
+ aws_config = { region: region }
83
+
84
+ # Use credentials from configuration if provided
85
+ config = NeetoWsPusher.configuration
86
+ if config.aws_access_key_id && config.aws_secret_access_key
87
+ aws_config[:credentials] = Aws::Credentials.new(
88
+ config.aws_access_key_id,
89
+ config.aws_secret_access_key
90
+ )
91
+ @logger.debug("Using configured AWS credentials")
92
+ else
93
+ @logger.debug("Using default AWS credential provider chain")
94
+ end
95
+
96
+ @sqs_client = Aws::SQS::Client.new(aws_config)
97
+ @logger.info("Connected to AWS SQS in region: #{region}")
98
+ end
99
+
100
+ def validate_parameters(message, user_id, channel)
101
+ errors = []
102
+ errors << "Message is required" unless message
103
+ errors << "Either user_id or channel must be provided" if user_id.nil? && channel.nil?
104
+
105
+ raise Error, errors.join(", ") if errors.any?
106
+ end
107
+
108
+ def parse_json(json_string)
109
+ JSON.parse(json_string)
110
+ rescue JSON::ParserError => e
111
+ raise Error, "Invalid JSON message: #{e.message}"
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+
5
+ module NeetoWsPusher
6
+ # Rails engine that handles WebSocket authentication
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace NeetoWsPusher
9
+
10
+ # Initialize any authentication callbacks
11
+ initializer "neeto_ws_pusher.setup_authentication" do |app|
12
+ # Can be extended to set up authentication integrations
13
+ end
14
+
15
+ config.generators do |g|
16
+ g.test_framework :rspec
17
+ g.assets false
18
+ g.helper false
19
+ end
20
+
21
+ # Load JavaScript assets
22
+ initializer "neeto_ws_pusher.assets" do |app|
23
+ app.config.assets.paths << root.join("app", "assets", "javascripts")
24
+ app.config.assets.precompile += %w(neeto_ws_pusher.js)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoWsPusher
4
+ # Module to extend User model with WebSocket functionality
5
+ module UserExtension
6
+ # Send a WebSocket message to this user
7
+ #
8
+ # @param message [Hash, String] The message to send
9
+ # @param channel [String] Optional channel name
10
+ # @return [String] Message ID if successful
11
+ # @example Send a notification
12
+ # user.send_ws_message(event: 'notification', content: 'Hello!')
13
+ # @example Send a notification to a specific channel
14
+ # user.send_ws_message({ event: 'notification', content: 'Hello!' }, channel: 'notifications')
15
+ def send_ws_message(message, channel: nil)
16
+ # Use the user's ID as the user_id
17
+ NeetoWsPusher.new.send_message(
18
+ user_id: id.to_s,
19
+ app_name: NeetoWsPusher.configuration.default_app_name,
20
+ message: message,
21
+ channel: channel
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "neeto_ws_pusher/model_extensions"
5
+
6
+ module NeetoWsPusher
7
+ # Rails integration for NeetoWsPusher
8
+ class Railtie < Rails::Railtie
9
+ # Automatically extend the User model with WebSocket functionality
10
+ initializer "neeto_ws_pusher.extend_user_model" do
11
+ ActiveSupport.on_load(:active_record) do
12
+ # If User model exists, extend it with WebSocket functionality
13
+ if defined?(::User)
14
+ ::User.include(NeetoWsPusher::UserExtension)
15
+ if NeetoWsPusher.configuration.logging_enabled
16
+ Rails.logger.info("[NeetoWsPusher][INFO]: Added WebSocket functionality to User model")
17
+ end
18
+ elsif NeetoWsPusher.configuration.logging_enabled
19
+ Rails.logger.warn("[NeetoWsPusher][WARN]: User model not found, WebSocket extensions not applied")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NeetoWsPusher
4
+ VERSION = "1.0.3"
5
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "neeto_ws_pusher/version"
4
+ require "neeto_ws_pusher/client"
5
+
6
+ # Load Rails integration if Rails is defined
7
+ if defined?(Rails)
8
+ require "neeto_ws_pusher/model_extensions"
9
+ require "neeto_ws_pusher/engine"
10
+ require "neeto_ws_pusher/railtie"
11
+ end
12
+
13
+ # Main module for the NeetoWsPusher gem
14
+ module NeetoWsPusher
15
+ # Custom error class for NeetoWsPusher errors
16
+ class Error < StandardError; end
17
+
18
+ # Configure the gem with default options
19
+ def self.configure
20
+ yield(configuration) if block_given?
21
+ configuration
22
+ end
23
+
24
+ # Get the current configuration
25
+ def self.configuration
26
+ @_configuration ||= Configuration.new
27
+ end
28
+
29
+ # Configuration class for storing default settings
30
+ class Configuration
31
+ attr_accessor :queue_url, :region,
32
+ :aws_access_key_id, :aws_secret_access_key,
33
+ :default_app_name, :logging_enabled
34
+
35
+ def initialize
36
+ # AWS SQS configuration
37
+ @queue_url = nil
38
+ @region = "us-east-1"
39
+ @aws_access_key_id = nil
40
+ @aws_secret_access_key = nil
41
+
42
+ # WebSocket authentication API configuration
43
+ @default_app_name = "default" # Default application name
44
+
45
+ # Logging configuration
46
+ @logging_enabled = true
47
+ end
48
+ end
49
+
50
+ # Helper method to create a new client
51
+ def self.new(options = {})
52
+ Client.new(options)
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: neeto_ws_pusher
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Neeto Engineers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-11-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-sqs
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: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 6.0.0
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '9.0'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 6.0.0
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '9.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: jwt
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: bundler
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '13.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rspec-rails
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '5.0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '5.0'
131
+ description: A lightweight Ruby gem for sending messages to WebSocket clients through
132
+ an AWS SQS queue. Includes Rails integration with WebSocket authentication endpoint
133
+ and User model extensions.
134
+ email:
135
+ - engineering@neeto.com
136
+ executables:
137
+ - neeto_ws_pusher
138
+ extensions: []
139
+ extra_rdoc_files: []
140
+ files:
141
+ - CHANGELOG.md
142
+ - LICENSE.md
143
+ - README.md
144
+ - app/assets/javascripts/neeto_ws_pusher.js
145
+ - app/controllers/neeto_ws_pusher/cookies_controller.rb
146
+ - app/controllers/neeto_ws_pusher/ws_auth_controller.rb
147
+ - bin/neeto_ws_pusher
148
+ - config/routes.rb
149
+ - lib/generators/neeto_ws_pusher/install/install_generator.rb
150
+ - lib/generators/neeto_ws_pusher/install/templates/initializer.rb
151
+ - lib/neeto_ws_pusher.rb
152
+ - lib/neeto_ws_pusher/client.rb
153
+ - lib/neeto_ws_pusher/engine.rb
154
+ - lib/neeto_ws_pusher/model_extensions.rb
155
+ - lib/neeto_ws_pusher/railtie.rb
156
+ - lib/neeto_ws_pusher/version.rb
157
+ homepage: https://github.com/bigbinary/neeto-ws-pusher
158
+ licenses:
159
+ - MIT
160
+ metadata:
161
+ homepage_uri: https://github.com/bigbinary/neeto-ws-pusher
162
+ source_code_uri: https://github.com/bigbinary/neeto-ws-pusher/tree/main
163
+ changelog_uri: https://github.com/bigbinary/neeto-ws-pusher/blob/main/CHANGELOG.md
164
+ documentation_uri: https://github.com/bigbinary/neeto-ws-pusher/blob/main/README.md
165
+ bug_tracker_uri: https://github.com/bigbinary/neeto-ws-pusher/issues
166
+ post_install_message:
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: 2.6.0
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubygems_version: 3.5.10
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: Send messages to WebSocket clients via AWS SQS
185
+ test_files: []