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 transpiled code is included for Node.js `4.x` and `0.12`
|
59 | compatibility).
|
60 |
|
61 |
|
62 | ## Table of Contents
|
63 |
|
64 | - [Background](#background)
|
65 | - [Installation](#installation)
|
66 | - [Usage](#usage)
|
67 | - [API](#api)
|
68 | - [Concepts overview](#concepts-overview)
|
69 | - [Customisation examples](#customisation-examples)
|
70 | - [Contribute](#contribute)
|
71 | - [License](#license)
|
72 |
|
73 |
|
74 | ## Background
|
75 |
|
76 | Read this article for more background information. (TODO)
|
77 |
|
78 |
|
79 | ## Installation
|
80 |
|
81 | This project is a [node](http://nodejs.org) module available via
|
82 | [npm](https://npmjs.com). Go check them out if you don't have them
|
83 | locally installed.
|
84 |
|
85 | ```sh
|
86 | $ npm i chat-service
|
87 | ```
|
88 |
|
89 |
|
90 | ## Usage
|
91 |
|
92 | ### Quickstart with socket.io
|
93 |
|
94 | First define a server configuration. On a server-side define a socket
|
95 | connection hook, as the service is relying on an extern auth
|
96 | implementation. A user just needs to pass an auth check, no explicit
|
97 | user adding step is required.
|
98 |
|
99 | ```javascript
|
100 | const ChatService = require('chat-service')
|
101 |
|
102 | const port = 8000
|
103 |
|
104 | function onConnect (service, id) {
|
105 | // Assuming that auth data is passed in a query string.
|
106 | let { query } = service.transport.getHandshakeData(id)
|
107 | let { userName } = query
|
108 | // Actually check auth data.
|
109 | // ...
|
110 | // Return a promise that resolves with a login string.
|
111 | return Promise.resolve(userName)
|
112 | }
|
113 | ```
|
114 |
|
115 | Creating a server is a simple object instantiation. Note: `close`
|
116 | method _must_ be called to correctly shutdown a service instance (see
|
117 | [Failures recovery](#failures-recovery)).
|
118 |
|
119 | ```javascript
|
120 | const chatService = new ChatService({port}, {onConnect})
|
121 |
|
122 | process.on('SIGINT', () => chatService.close().finally(() => process.exit()))
|
123 | ```
|
124 |
|
125 | Server is now running on port `8000`, using `memory` state. By default
|
126 | `'/chat-service'` socket.io namespace is used. Add a room with `admin`
|
127 | user as the room owner. All rooms must be explicitly created (option
|
128 | to allow rooms creation from a client side is also provided).
|
129 |
|
130 | ```javascript
|
131 | // It is an error to add the same room twice. The room configuration
|
132 | // and messages will persist if redis state is used.
|
133 | chatService.hasRoom('default').then(hasRoom => {
|
134 | if (!hasRoom) {
|
135 | return chatService.addRoom('default', { owner: 'admin' })
|
136 | }
|
137 | })
|
138 | ```
|
139 |
|
140 | On a client just a `socket.io-client` implementation is required. To
|
141 | send a request (command) use `emit` method, the result (or an error)
|
142 | will be returned in socket.io ack callback. To listen to server
|
143 | messages use `on` method.
|
144 |
|
145 | ```javascript
|
146 | const io = require('socket.io-client')
|
147 |
|
148 | // Use https or wss in production.
|
149 | let url = 'ws://localhost:8000/chat-service'
|
150 | let userName = 'user' // for example and debug
|
151 | let token = 'token' // auth token
|
152 | let query = `userName=${userName}&token=${token}`
|
153 | let opts = { query }
|
154 |
|
155 | // Connect to a server.
|
156 | let socket = io.connect(url, opts)
|
157 |
|
158 | // Rooms messages handler (own messages are here too).
|
159 | socket.on('roomMessage', (room, msg) => {
|
160 | console.log(`${msg.author}: ${msg.textMessage}`)
|
161 | })
|
162 |
|
163 | // Auth success handler.
|
164 | socket.on('loginConfirmed', userName => {
|
165 | // Join room named 'default'.
|
166 | socket.emit('roomJoin', 'default', (error, data) => {
|
167 | // Check for a command error.
|
168 | if (error) { return }
|
169 | // Now we will receive 'default' room messages in 'roomMessage' handler.
|
170 | // Now we can also send a message to 'default' room:
|
171 | socket.emit('roomMessage', 'default', { textMessage: 'Hello!' })
|
172 | })
|
173 | })
|
174 |
|
175 | // Auth error handler.
|
176 | socket.on('loginRejected', error => {
|
177 | console.error(error)
|
178 | })
|
179 | ```
|
180 |
|
181 | It is a runnable code, files are in `example` directory.
|
182 |
|
183 | ### Debugging
|
184 |
|
185 | Under normal circumstances all errors that are returned to a service
|
186 | user (via request replies, `loginConfirmed` or `loginRejected`
|
187 | messages) are instances of `ChatServiceError`. All other errors
|
188 | indicate a program bug or a failure in a service infrastructure. To
|
189 | enable debug logging of such errors use `export
|
190 | NODE_DEBUG=ChatService`. The library is using bluebird `^3.0.0`
|
191 | promises implementation, so to enable long stack traces use `export
|
192 | BLUEBIRD_DEBUG=1`. It is highly recommended to use promise versions of
|
193 | APIs for hooks and `ChatServiceError` subclasses for hook errors.
|
194 |
|
195 |
|
196 | ## API
|
197 |
|
198 | Server side
|
199 | [API](https://an-sh.github.io/chat-service/0.9/chat-service.html) and
|
200 | [RPC](https://an-sh.github.io/chat-service/0.9/rpc.html) documentation
|
201 | is available online.
|
202 |
|
203 |
|
204 | ## Concepts overview
|
205 |
|
206 | ### User multiple connections
|
207 |
|
208 | Service completely abstracts a connection concept from a user concept,
|
209 | so a single user can have more than one connection (including
|
210 | connections across different nodes). For user presence the number of
|
211 | joined sockets must be just greater than zero. All APIs designed to
|
212 | work on the user level, handling seamlessly user's multiple
|
213 | connections.
|
214 |
|
215 | Connections are completely independent, no additional client side
|
216 | support is required. But there are info messages and commands that can
|
217 | be used to get information about other user's connections. It makes
|
218 | possible to realise client-side sync patterns, like keeping all
|
219 | connections to be joined to the same rooms.
|
220 |
|
221 | ### Room permissions
|
222 |
|
223 | Each room has a permissions system. There is a single owner user, that
|
224 | has all administrator privileges and can assign users to the
|
225 | administrators group. Administrators can manage other users' access
|
226 | permissions. Two modes are supported: blacklist and whitelist. After
|
227 | access lists/mode modifications, service automatically removes users
|
228 | that have lost an access permission.
|
229 |
|
230 | If `enableRoomsManagement` options is enabled users can create rooms
|
231 | via `roomCreate` command. The creator of a room will be it's owner and
|
232 | can also delete it via `roomDelete` command.
|
233 |
|
234 | Before hooks can be used to implement additional permissions systems.
|
235 |
|
236 | ### Reliable messaging and history synchronisation
|
237 |
|
238 | When a user sends a room message, in RPC reply the message `id` is
|
239 | returned. It means that the message has been saved in a store (in an
|
240 | append only circular buffer like structure). Room message ids are a
|
241 | sequence starting from `1`, that increases by one for each
|
242 | successfully sent message in the room. A client can always check the
|
243 | last room message id via `roomHistoryInfo` command, and use
|
244 | `roomHistoryGet` command to get missing messages. Such approach
|
245 | ensures that a message can be received, unless it is deleted due to
|
246 | rotation.
|
247 |
|
248 | ### Custom messages format
|
249 |
|
250 | By default a client can send messages that are limited to just a
|
251 | `{textMessage: 'Some string'}`. To enable custom messages format
|
252 | provide `directMessagesChecker` or `roomMessagesChecker` hooks. When a
|
253 | hook resolves, a message format is accepted. Messages can be arbitrary
|
254 | data with a few restrictions. The top level must be an `Object`,
|
255 | without `timestamp`, `author` or `id` fields (service will fill this
|
256 | fields before sending messages). The nested levels can include
|
257 | arbitrary data types (even binary), but no nested objects with a field
|
258 | `type` set to `'Buffer'` (used for binary data manipulations).
|
259 |
|
260 | ### Integration and customisations
|
261 |
|
262 | Each user command supports before and after hook adding, and a client
|
263 | connection/disconnection hooks are supported too. Command and hooks
|
264 | are executed sequentially: before hook - command - after hook (it will
|
265 | be called on command errors too). Sequence termination in before hooks
|
266 | is possible. Clients can send additional command arguments, hooks can
|
267 | read them, and reply with additional arguments.
|
268 |
|
269 | To execute an user command server side `execUserCommand` is
|
270 | provided. Also there are some more server side only methods provided
|
271 | by `ServiceAPI` and `TransportInterface`. Look for some customisation
|
272 | cases in [Customisation examples](#customisation-examples).
|
273 |
|
274 | ### Failures recovery
|
275 |
|
276 | Service keeps user presence and connection data in a store, that may
|
277 | be persistent or shared. So if an instance is shutdown incorrectly
|
278 | (without calling or waiting for `close` method to finish) or lost
|
279 | completely network connection to a store, presence data will become
|
280 | incorrect. To fix this case `instanceRecovery` method is provided.
|
281 |
|
282 | Also there are more subtle cases regarding connection-dependant data
|
283 | consistency. Transport communication instances and store instances can
|
284 | experience various kind of network, software or hardware failures. In
|
285 | some edge cases (like operation on multiple users) such failures can
|
286 | cause inconsistencies (for the most part errors will be returned to
|
287 | the command's issuers). Such events are reported via instance events
|
288 | (like `storeConsistencyFailure`), and data can be sync via
|
289 | `RecoveryAPI` methods.
|
290 |
|
291 |
|
292 | ## Customisation examples
|
293 |
|
294 | ### Anonymous listeners
|
295 |
|
296 | By default every user is assumed to have an unique login
|
297 | (userName). Instead of managing names generation, an integration with
|
298 | a separate transport can be used (or a multiplexed connection, for
|
299 | example an another socket.io namespace). Room messages can be
|
300 | forwarded from `roomMessage` after hook to a transport, that is
|
301 | accessible without a login. And vice versa some service commands can
|
302 | be executed by anonymous users via `execUserCommand` with bypassing
|
303 | permissions option turned on.
|
304 |
|
305 | ### Messages aggregation and filtering
|
306 |
|
307 | A `roomMessage` after hook can be also used to forward messages from
|
308 | one room to another. So rooms can be used for messages aggregation
|
309 | from another rooms. Since hooks are just functions and have a full
|
310 | access to messages content, it allows to implement arbitrary
|
311 | content-based forwarding rules. Including implementing systems with
|
312 | highly personalised user (client) specific feeds.
|
313 |
|
314 | ### Explicit multi-device announcements
|
315 |
|
316 | By default there is no way for other users to know the number and
|
317 | types of user connections joined to a room. Such information can be
|
318 | passed, for example in a query string and then saved via a connection
|
319 | hook. The announcement can be made in `roomJoin` after hook, using
|
320 | directly transport `sendToChannel` method. Also additional information
|
321 | regarding joined devices types should be sent from `roomGetAccessList`
|
322 | after hook (when list name is equal to `'userlist'`).
|
323 |
|
324 |
|
325 | ## Contribute
|
326 |
|
327 | If you encounter a bug in this package, please submit a bug report to
|
328 | github repo [issues](https://github.com/an-sh/chat-service/issues).
|
329 |
|
330 | PRs are also accepted.
|
331 |
|
332 |
|
333 | ## License
|
334 |
|
335 | MIT
|