1 |
|
2 | # Chat Service
|
3 |
|
4 | [![NPM Version](https://badge.fury.io/js/chat-service.svg)](https://badge.fury.io/js/chat-service)
|
5 | [![Build Status](https://travis-ci.org/an-sh/chat-service.svg?branch=master)](https://travis-ci.org/an-sh/chat-service)
|
6 | [![Appveyor status](https://ci.appveyor.com/api/projects/status/qy7v2maica2urkss?svg=true)](https://ci.appveyor.com/project/an-sh/chat-service)
|
7 | [![Coverage Status](https://codecov.io/gh/an-sh/chat-service/branch/master/graph/badge.svg)](https://codecov.io/gh/an-sh/chat-service)
|
8 | [![Dependency Status](https://david-dm.org/an-sh/chat-service.svg)](https://david-dm.org/an-sh/chat-service)
|
9 | [![JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/)
|
10 |
|
11 | Room messaging server implementation that is using a bidirectional RPC
|
12 | protocol to implement chat-like communication. Designed to handle
|
13 | common public network messaging problems like reliable delivery,
|
14 | multiple connections from a single user, real-time permissions and
|
15 | presence. RPC requests processing and a room messages format are
|
16 | customisable via hooks, allowing to implement anything from a
|
17 | chat-rooms server to a collaborative application with a complex
|
18 | conflict resolution. Room messages also can be used to create public
|
19 | APIs or to tunnel M2M communications for IoT devices.
|
20 |
|
21 |
|
22 | ## Features
|
23 |
|
24 |
|
25 | - Reliable room messaging using a server side history storage and a
|
26 | synchronisation API.
|
27 |
|
28 | - Arbitrary messages format via just a validation function (hook),
|
29 | allowing custom/heterogeneous messages formats (including a binary
|
30 | data inside messages).
|
31 |
|
32 | - Per-room user presence API with notifications.
|
33 |
|
34 | - Realtime room creation and per-room users permissions management
|
35 | APIs. Supports for blacklist or whitelist based access modes and an
|
36 | optional administrators group.
|
37 |
|
38 | - Seamless support of multiple users' connections from various devises
|
39 | to any service instance.
|
40 |
|
41 | - Written as a stateless microservice, uses Redis (also supports
|
42 | cluster configurations) as a state store, can be horizontally scaled
|
43 | on demand.
|
44 |
|
45 | - Extensive customisation support. Custom functionality can be added
|
46 | via hooks before/after for any client request processing. And
|
47 | requests (commands) handlers can be invoked server side via an API.
|
48 |
|
49 | - Pluginable networking transport. Client-server communication is done
|
50 | via a bidirectional RPC protocol. Socket.io transport implementation
|
51 | is included.
|
52 |
|
53 | - Pluginable state store. Memory and Redis stores are included.
|
54 |
|
55 | - Supports lightweight online user to online user messaging.
|
56 |
|
57 | - Now fully rewritten in __ES6__, runs natively on Node.js `>= 6` (ES5
|
58 | Babel transpired code is included for Node.js `4.x` and `0.12`
|
59 | compatibility).
|
60 |
|
61 |
|
62 | ## Quickstart with socket.io
|
63 |
|
64 | On a server-side define a socket connection hook, as the service is
|
65 | relying on an extern auth implementation. A user just needs to pass an
|
66 | auth check, no explicit user adding step is required.
|
67 |
|
68 | ```javascript
|
69 | function onConnect (service, id) {
|
70 | // Assuming that auth data is passed in a query string.
|
71 | let { query } = service.transport.getHandshakeData(id)
|
72 | let { userName } = query
|
73 | // Actually check auth data.
|
74 | // ...
|
75 | // Return a promise that resolves with a login string.
|
76 | return Promise.resolve(userName)
|
77 | }
|
78 | ```
|
79 |
|
80 | Creating a server is a simple object instantiation. Note: `close`
|
81 | method _must_ be called to correctly shutdown a service instance (see
|
82 | [Failures recovery](#failures-recovery)).
|
83 |
|
84 | ```javascript
|
85 | const port = 8000
|
86 | const ChatService = require('chat-service')
|
87 | const chatService = new ChatService({port}, {onConnect})
|
88 | process.on('SIGINT', () => chatService.close().finally(() => process.exit()))
|
89 | ```
|
90 |
|
91 | Server is now running on port `8000`, using memory state. By default
|
92 | `'/chat-service'` socket.io namespace is used. Add a room with `admin`
|
93 | user as the room owner. All rooms must be explicitly created (option
|
94 | to allow rooms creation from a client side is also provided).
|
95 |
|
96 | ```javascript
|
97 | chatService.addRoom('default', { owner: 'admin' })
|
98 | ```
|
99 |
|
100 | On a client just a `socket.io-client` implementation is required. To
|
101 | send a request (command) use `emit` method, the result (or an error)
|
102 | will be returned in socket.io ack callback. To listen to server
|
103 | messages use `on` method.
|
104 |
|
105 | ```javascript
|
106 | let io = require('socket.io-client')
|
107 | let url = 'ws://localhost:8000/chat-service'
|
108 | let userName = 'user'
|
109 | let token = 'token' // auth token
|
110 | let query = `userName=${userName}&token=${token}`
|
111 | let params = { query }
|
112 | // Connect to server.
|
113 | let socket = io.connect(url, params)
|
114 | socket.once('loginConfirmed', userName => {
|
115 | // Auth success.
|
116 | socket.on('roomMessage', (room, msg) => {
|
117 | // Rooms messages handler (own messages are here too).
|
118 | console.log(`${msg.author}: ${msg.textMessage}`)
|
119 | })
|
120 | // Join room 'default'.
|
121 | socket.emit('roomJoin', 'default', (error, data) => {
|
122 | // Check for a command error.
|
123 | if (error) return
|
124 | // Now we will receive 'default' room messages in 'roomMessage' handler.
|
125 | // Now we can also send a message to 'default' room:
|
126 | socket.emit('roomMessage', 'default', { textMessage: 'Hello!' })
|
127 | })
|
128 | })
|
129 | socket.once('loginRejected', error => {
|
130 | // Auth error handler.
|
131 | console.error(error)
|
132 | })
|
133 | ```
|
134 |
|
135 | It is a runnable code, files are in `example` directory. For more
|
136 | details and advanced usage see [Documentation](#documentation).
|
137 |
|
138 |
|
139 | ## Concepts overview
|
140 |
|
141 | ### User multiple connections
|
142 |
|
143 | Service completely abstracts a connection concept from a user concept,
|
144 | so a single user can have more than one connection (including
|
145 | connections across different nodes). For user presence the number of
|
146 | joined sockets must be just greater than zero. All APIs designed to
|
147 | work on the user level, handling seamlessly user's multiple
|
148 | connections.
|
149 |
|
150 | Connections are completely independent, no additional client side
|
151 | support is required. But there are info messages and commands that can
|
152 | be used to get information about other user's connections. It makes
|
153 | possible to realise client-side sync patterns, like keeping all
|
154 | connections to be joined to the same rooms.
|
155 |
|
156 | ### Room permissions
|
157 |
|
158 | Each room has a permissions system. There is a single owner user, that
|
159 | has all administrator privileges and can assign users to the
|
160 | administrators group. Administrators can manage other users' access
|
161 | permissions. Two modes are supported: blacklist and whitelist. After
|
162 | access lists/mode modifications, service automatically removes users
|
163 | that have lost an access permission.
|
164 |
|
165 | If `enableRoomsManagement` options is enabled users can create rooms
|
166 | via `roomCreate` command. The creator of a room will be it's owner and
|
167 | can also delete it via `roomDelete` command.
|
168 |
|
169 | Before hooks can be used to implement additional permissions systems.
|
170 |
|
171 | ### Reliable messaging and history synchronisation
|
172 |
|
173 | When a user sends a room message, in RPC reply the message `id` is
|
174 | returned. It means that the message has been saved in a store (in an
|
175 | append only circular buffer like structure). Room message ids are a
|
176 | sequence starting from `1`, that increases by one for each
|
177 | successfully sent message in the room. A client can always check the
|
178 | last room message id via `roomHistoryInfo` command, and use
|
179 | `roomHistoryGet` command to get missing messages. Such approach
|
180 | ensures that a message can be received, unless it is deleted due to
|
181 | rotation.
|
182 |
|
183 | ### Custom messages format
|
184 |
|
185 | By default a client can send messages that are limited to just a
|
186 | `{textMessage: 'Some string'}`. To enable custom messages format
|
187 | provide `directMessagesChecker` or `roomMessagesChecker` hooks. When a
|
188 | hook resolves, a message format is accepted. Messages can be arbitrary
|
189 | data with a few restrictions. The top level must be an `Object`,
|
190 | without `timestamp`, `author` or `id` fields (service will fill this
|
191 | fields before sending messages). The nested levels can include
|
192 | arbitrary data types (even binary), but no nested objects with a field
|
193 | `type` set to `'Buffer'` (used for binary data manipulations).
|
194 |
|
195 | ### Integration and customisations
|
196 |
|
197 | Each user command supports before and after hook adding, and a client
|
198 | connection/disconnection hooks are supported too. Command and hooks
|
199 | are executed sequentially: before hook - command - after hook (it will
|
200 | be called on command errors too). Sequence termination in before hooks
|
201 | is possible. Clients can send additional command arguments, hooks can
|
202 | read them, and reply with additional arguments.
|
203 |
|
204 | To execute an user command server side `execUserCommand` is
|
205 | provided. Also there are some more server side only methods provided
|
206 | by `ServiceAPI` and `TransportInterface`. Look for some customisation
|
207 | cases in [Customisation examples](#customisation-examples).
|
208 |
|
209 | ### Failures recovery
|
210 |
|
211 | Service keeps user presence and connection data in a store, that may
|
212 | be persistent or shared. So if an instance is shutdown incorrectly
|
213 | (without calling or waiting for `close` method to finish) or lost
|
214 | completely network connection to a store, presence data will become
|
215 | incorrect. To fix this case `instanceRecovery` method is provided.
|
216 |
|
217 | Also there are more subtle cases regarding connection-dependant data
|
218 | consistency. Transport communication instances and store instances can
|
219 | experience various kind of network, software or hardware failures. In
|
220 | some edge cases (like operation on multiple users) such failures can
|
221 | cause inconsistencies (for the most part errors will be returned to
|
222 | the command's issuers). Such events are reported via instance events
|
223 | (like `storeConsistencyFailure`), and data can be sync via
|
224 | `RecoveryAPI` methods.
|
225 |
|
226 |
|
227 | ## Customisation examples
|
228 |
|
229 | ### Anonymous listeners
|
230 |
|
231 | By default every user is assumed to have an unique login
|
232 | (userName). Instead of managing names generation, an integration with
|
233 | a separate transport can be used (or a multiplexed connection, for
|
234 | example an another socket.io namespace). Room messages can be
|
235 | forwarded from `roomMessage` after hook to a transport, that is
|
236 | accessible without a login. And vice versa some service commands can
|
237 | be executed by anonymous users via `execUserCommand` with bypassing
|
238 | permissions option turned on.
|
239 |
|
240 | ### Explicit multi-device announcements
|
241 |
|
242 | By default there is no way for other users to know the number and
|
243 | types of user connections joined to a room. Such information can be
|
244 | passed, for example in a query string and then saved via a connection
|
245 | hook. The announcement can be made in `roomJoin` after hook, using
|
246 | directly transport `sendToChannel` method. Also additional information
|
247 | regarding joined devices types should be sent from `roomGetAccessList`
|
248 | after hook (when list name is equal to `'userlist'`).
|
249 |
|
250 |
|
251 | ## Documentation
|
252 |
|
253 | Server-side and RPC APIs documentation is available
|
254 | [online](https://an-sh.github.io/chat-service/0.9/).
|
255 |
|
256 |
|
257 | ## Debugging
|
258 |
|
259 | Under normal circumstances all errors that are returned to a service
|
260 | user (via request replies, `loginConfirmed` or `loginRejected`
|
261 | messages) should be instances of `ChatServiceError`. All other errors
|
262 | indicate a bug or a failure in a service infrastructure. To enable
|
263 | debug logging of such errors use `export NODE_DEBUG=ChatService`. The
|
264 | library is using bluebird `^3.0.0` promises implementation, so to
|
265 | enable long stack traces use `export BLUEBIRD_DEBUG=1`. It is highly
|
266 | recommended to use promise versions of APIs for hooks.
|
267 |
|
268 | ## Bug reporting
|
269 |
|
270 | If you encounter a bug in this package, please submit a bug report to
|
271 | github repo [issues](https://github.com/an-sh/chat-service/issues).
|
272 |
|
273 |
|
274 | ## License
|
275 |
|
276 | MIT
|