UNPKG

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