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 +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE.md +22 -0
- data/README.md +212 -0
- data/app/assets/javascripts/neeto_ws_pusher.js +241 -0
- data/app/controllers/neeto_ws_pusher/cookies_controller.rb +13 -0
- data/app/controllers/neeto_ws_pusher/ws_auth_controller.rb +21 -0
- data/bin/neeto_ws_pusher +129 -0
- data/config/routes.rb +6 -0
- data/lib/generators/neeto_ws_pusher/install/install_generator.rb +62 -0
- data/lib/generators/neeto_ws_pusher/install/templates/initializer.rb +48 -0
- data/lib/neeto_ws_pusher/client.rb +114 -0
- data/lib/neeto_ws_pusher/engine.rb +27 -0
- data/lib/neeto_ws_pusher/model_extensions.rb +25 -0
- data/lib/neeto_ws_pusher/railtie.rb +24 -0
- data/lib/neeto_ws_pusher/version.rb +5 -0
- data/lib/neeto_ws_pusher.rb +54 -0
- metadata +185 -0
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
|
data/bin/neeto_ws_pusher
ADDED
|
@@ -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,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,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: []
|