Создание кастомных чатов с Firebase Realtime database

Во многих мобильных и веб-приложениях требуется создать чаты. Мы говорим не о надоевших всплывашках для общения с оператором: обычно они предоставляются As a service и подключаются одной строчкой кода.

Я говорю о полноценных чатах, в том числе групповых, где пользователи могут общаться между собой, отправлять медиа, использовать видеозвонки. Подчеркну, что речь идёт о приложениях, где чат является лишь малой частью ПО, а не его основным функционалом — не требуется offline режим, E2E шифрование, и многие другие вещи (как в мессенджерах Telegram, WhatsApp и т. д.).

Создание кастомных чатов с Firebase Realtime database

Технически любой чат состоит из следующих вещей: UI, транспортный уровень, сервер, хранилище. В качестве транспорта для чатов лучше всего подходят веб-сокеты. Хранилищем может выступать любая база данных, сервер должен обеспечивать права доступа и API для клиентов.

Стандартных решений для чатов множество, одним из наиболее популярных и проверенных временем можно назвать Jabber (XMPP), имеющий множество реализаций. Также популярны кастомные решения на основе Node.js и socket.io, а также Firebase. В этой статье не будет сравнений разных подходов, а пойдет речь о реализации чатов с помощью Firebase realtime database.

Делаем чаты с Firebase realtime database

Firebase realtime database (RDB) — nosql база данных, размещённая на серверах Google и доступная по сети по протоколу Websocket, есть SDK для практически всех платформ и языков. Стоит отметить, что Firebase недоступен в Китае и ряде других стран, где сервера Google заблокированы фаерволами.

RDB покрывает 3 из 4 компонентов любого чата: транспорт, сервер, хранилище. Фактически фронтенд напрямую соединяется с базой данных и может как читать так и записывать туда документы (сообщения, пользователей, комнаты чатов). Доступ пользователя до документов регулируется Security rules. При соединении с базой данных клиент авторизуется по JWT токену, в котором зашит userId или другая информация о нём, и с помощью выражений security rules, можно описать, имеет ли он доступ до того или иного документа или коллекции. Также security rules используются для индексации документов.

Вот пример security rules из опенсорсного проекта Firechat:

{ "rules": { ".read": false, ".write": false, "messages": { "$roomId": { ".indexOn": ["ts"], "$messageId": { ".indexOn": "ts" ".read": "(auth != null) && ((root.child('rooms').child($roomId).child('authorizedUsers').hasChild(auth.uid)))" ".write": "(auth != null) && ((root.child('rooms').child($roomId).child('authorizedUsers').hasChild(auth.uid)))" } } }, }

Смысл этих выражений в том, что пользователь может получить доступ до объекта внутри коллекции messages только в том случае, если uid из его JWT токена есть в списке authorizedUsers соответствующей комнаты чата

Сама схема базы данных в Firebase состоит из следующих коллекций, и, конечно, зависит от бизнес-требований к чату. Примерный вид схемы:

users: { $userId: { rooms: [], // массив id комнат, в которых участвует пользователь userInfo: {} // произвольная информация о пользователе, зависит от ваших потребностей. Может содержать имя, url аватарки и так далее. } }, rooms: { $roomId: { id, // совпадает с roomId, сгенерирован Firebase type, // тип комнаты, персональная или групповая ts, // время создания authorizedUsers: [] // массив uid авторизованных пользователей moderators: [] // массив uid модераторов чата } }, messages: { $roomId: { // внутри коллекции с ID=roomId хранятся все её сообщения $messageId: { id, type, // тип сообщения, текстовое, фото, видео ts, senderId, readBy: [], // массив uid пользователей, прочитавших сообщение. Массив, потому что в групповых чатах пользователей в комнате может быть много. deliveredTo: [], // массив uid пользователей, получивших сообщение } } }

Пример авторизации в RDB с помощью Javascript SDK:

firebase.auth().signInWithCustomToken(this.jwt).catch(function (error) { alert('Error: ' + error.message); });

JWT токен можно сгенерировать на бэкенде вашего приложения и отдавать пользователю. Например, в методе login, вместе с токеном вашего сервера.

Пример генерации JWT-токена с помощью SDK PHP:

use Firebase\JWT\JWT; $now = time(); $payload = [ 'email' => $user->getEmail(), 'iss' => $this->clientEmail, 'sub' => $this->clientEmail, 'aud' => 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit', 'iat' => $now, 'exp' => ($now + (60 * 60)), // Maximum expiration time is one hour 'uid' => (string) $user->getId(), ]; return JWT::encode($payload, $this->privateKey, 'RS256');

clientEmail и privateKey берутся из сервисного файла Firebase.

Примеры интеграции с Firebase на фронтенде

Загрузка списка id комнат пользователя
Для загрузки сообщений из комнаты чатов:

async getUserRoomIds(userId) { const snapshot = await firebase.database() .ref().child('users') .child(userId.toString()) .once('value'); const user = snapshot.val(); if (user == null) { return []; } return Object.keys(user.rooms); }

Для записи сообщения в чат мы просто записываем сообщение в таблицу messages новый объект:

async postMessage(roomId, messageStr, user) { const ref = firebase.database() .ref() .child('messages') .child(roomId) .push(); const message = new Message(user, firebase.database.ServerValue.TIMESTAMP, messageStr); message.id = ref.key; try { await ref.set(message.getJson()); } catch (e) { alert(e.message); throw e; } }

Частно нужно взаимодействовать с чатами на сервере (например, отправлять пуш нотификации при отправке нового сообщения). Используя Admin SDK, можно подписаться на любые новые сообщения в базе, минуя security rules. Вот пример подписки на новые сообщения в комнате чата:

async subscribeToRoom(id: string): Promise<void> { const room = await this.getRoom(id); const query = this.messages .child(id); query.on('child_added', this.onChildAdded(id, room), this.onCancel, this); } onChildAdded(roomId: string, room: Room): (snapshot: DataSnapshot) => void { return async (snapshot: DataSnapshot): Promise<void> => { const message = snapshot.val(); console.debug('(onAdded) message ' + message.id + ' in room ' + roomId); } } onCancel(): void { console.debug('Listener was canceled.'); }

Из минусов Firebase RDB можно отметить очень ограниченный набор функций по поиску данных. Например, полноценный поиск по сообщениям сделать там невозможно, для этого нужно либо вести двойную запись сообщений (как вариант, в Elasticsearch или Algolia). Двойную запись можно сделать как на своем сервере в событии child_added, так и с помощью Firebase cloud functions. Пример интеграции с Algolia.

Firebase имеет очень хорошую документацию и позволяет сделать кастомные чаты с практически любым функционалом. Из главных его плюсов — простота и отсутствие забот об инфраструктуре. А о том, как добавить в чаты видеозвонки, мы рассказывали в прошлой статье.

77
Начать дискуссию