Использование Firebase для отправки уведомлений в реальном времени в приложениях Django


Введение

Недавно я изучал несколько способов добавления функциональности реального времени в приложения Django. Во время поиска я наткнулся на множество сторонних сервисов, таких как PubNub, Pusher, а теперь и Firebase, который я считаю самым интересным из всех.

Ниже приведены критерии, которые я стандартизировал во время принятия решения и сравнения с пользовательскими решениями, такими как django channels. Давайте посмотрим, как Firebase соответствует этим пунктам.

  • Масштабируемость : Firebase база данных в реальном времени и firestore, оба могут масштабироваться автоматически.
  • Безопасность : Firebase имеет правила безопасности, которые могут быть установлены точно для авторизации ресурсов, и их настройка занимает всего несколько минут. Так что за это большой палец вверх.
  • Время на реализацию :  Firebase admin SDK хорошо документированы и доступны почти для всех серверных языков. Поэтому вы можете добавить функции реального времени за считанные минуты.
  • Удобство обслуживания :С Firebase не требуется абсолютно никакого обслуживания, за исключением некоторых правил безопасности, которые вы, возможно, захотите подправить по мере добавления новых функций.
  • Стоимость: Это может быть решающим фактором и может варьироваться от команды к команде или даже от одного проекта к другому. Firebase имеет щедрый бесплатный уровень и планы с оплатой по факту.

🚀 Давайте создадим демонстрационное приложение Django

В этой статье я проведу вас через все шаги, которые необходимо выполнить, чтобы успешно использовать firebase firestore для безопасной отправки уведомлений в реальном времени пользователям в ваших django приложениях.
В ходе тестирования я создал демонстрационное приложение, к которому вы можете обратиться в любой момент, когда окажетесь в затруднительном положении.
Вот ссылка на Github 😸 👇

mabdullahadeel / django-firebase-notifications

Использование firebase для отправки уведомлений в реальном времени в приложениях django

Django Firebase Notifications

Это простое django приложение, которое демонстрирует, как добавить уведомления в реальном времени в django приложения с помощью firebase.


Посмотреть на GitHub

Вот живое приложение 👇

https://django-firebase.vercel.app

Логика сервера

Первая проблема, с которой вы можете столкнуться при попытке внедрить firebase в ваше django приложение — это аутентификация. Ваши пользователи в настоящее время проходят аутентификацию в вашем django приложении, как вы передадите состояние аутентификации в firebase, чтобы он знал о пользователе?
Для решения этой проблемы есть пользовательские токены. Когда клиент успешно авторизуется в вашем django приложении, firebase admin SDK может быть использован для создания токена для клиента. Клиент будет использовать этот токен для подключения к firebase.
Вот как выглядит этот поток.

Полную схему можно посмотреть здесь

Начнем с входа пользователя в систему. Они вводят свои учетные данные для входа во внешнее приложение. Клиент отправляет учетные данные, т.е. имя пользователя/электронную почту и пароль, на бэкэнд для обмена на токен (JWT или простой токен).
Фактическая реализация аутентификации может варьироваться от приложения к приложению, но в моей демонстрации я использую аутентификацию на основе токенов DRF. При успешном входе сервер генерирует два токена, как показано на схеме выше.

  • «token» — Этот токен будет использоваться клиентом для аутентификации на сервере django при последующих запросах.
  • «firebase_token» — Этот токен будет использоваться клиентом для немедленной автоматической аутентификации на firebase.

Отправка уведомления

Давайте пройдемся по настройкам.

Здесь я предполагаю, что у вас есть аккаунт firebase, что вы и делаете, если у вас есть аккаунт google. Для отправки уведомлений из приложения django мы будем использовать firestore. Если вы еще не настроили проект firebase или firestore, я бы рекомендовал просмотреть это краткое руководство. 

Чтобы взаимодействовать с firebase через SDK администратора, вам нужны учетные данные учетной записи сервиса. Чтобы создать учетные данные учетной записи службы, перейдите к учетной записи службы в консоли проекта firebase. Убедитесь, что вы выбрали правильный проект. Выполните следующие действия 👇.

В случае успеха у вас будет загружен файл .json. Давайте переименуем этот файл в credentials.json . Обязательно сохраните этот файл, поскольку учетные данные в этом файле дают доступ ко всем ресурсам firebase.

Настройка экземпляра SDK администратора

Выполните следующую команду pip для установки firebase SDK в ваш проект django.

pip install firebase-admin
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Затем инициализируйте экземпляр приложения admin, указав путь к файлу credential.json. Если этот файл находится в директории вашего проекта. Обязательно добавьте credentials.json в ваш файл .gitignore.
В моем случае я создал вспомогательный класс FirebaseService в core/firebase/firebase_service.py для обработки всей логики, связанной с firebase.

import logging
from typing import Any, Dict
from uuid import uuid4
from django.conf import settings
from django.core.cache import cache

import firebase_admin
from firebase_admin import credentials, auth, firestore

from users.models import User

cred = credentials.Certificate(settings.GOOGLE_APPLICATION_CREDENTIALS) # path to credentials.json file

firebase_app = firebase_admin.initialize_app(cred)
auth_client = auth.Client(app=firebase_app)
firestore_client = firestore.client(app=firebase_app)

logger = logging.getLogger(__name__)


def cached(func):
    def wrapper(*args, **kwargs):
        user = kwargs.get('user')
        key = 'token_' + str(user.id)
        token = cache.get(key)
        if token is None:
            token = func(*args, **kwargs)
            cache.set(key, token, timeout=60 * 60) # 1 hour
        return token

    return wrapper

class FirebaseService:
  @staticmethod
  @cached
  def get_custom_token_for_user(user: User):
    auth_claims = {
      'uid': user.id,
    }
    return auth_client.create_custom_token(uid=user.id, developer_claims=auth_claims)

  @staticmethod
  def send_notification_to_user(user: User, message: Dict[str, Any]):
    msg_id = str(uuid4())
    notification_ref = firestore_client.collection(u'app-notifications') 
      .document(u'{}'.format(user.id)).collection("user-notifications").document(u'{}'.format(msg_id))

    notification_ref.set({
      u'message': message,
      'id': msg_id
    })
    logger.info(u'Notification sent to user {}'.format(user.id))

Вход в полноэкранный режим Выход из полноэкранного режима

Здесь я загружаю GOOGLE_APPLICATION_CRENDITALS, которая является путем к файлу credentials.json из настроек django. А в моем файле settings.py я загружаю ту же переменную из окружения с помощью django-environ. Вы можете взглянуть на это здесь.

  • Метод get_custom_token_for_user отвечает за создание токенов для данного пользователя django. Этот токен затем отправляется клиенту для использования при аутентификации на firebase. create_custom_token из админки firebase принимает аргумент developer_claims. Все, что вы передали сюда, будет сохранено в полезной нагрузке токена. Но не только это, этот объект/диск будет доступен в объекте firebase request.auth. Это означает, что вы можете получить доступ к этому объекту на клиенте и даже в правилах безопасности firebase.

Вот определение правила firestore, если вам интересно.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match/app-notifications/{user_id}/{document=**} {
      allow delete, read: if
          request.auth.uid == user_id
    }
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Логика на стороне клиента

На клиенте установите firebase.

yarn add firebase
# or
npm install firebase
# or
pnpm add firebase
Войти в полноэкранный режим Выйдите из полноэкранного режима

Затем в [./firebase/index.ts](https://github.com/mabdullahadeel/django-firebase-notifications/blob/master/client/firebase/index.ts) я использую учетные данные для инициализации приложения firebase и использую их во всем приложении.


import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FB_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FB_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FB_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FB_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FB_MSG_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FB_APP_ID,
};

export const app = initializeApp(firebaseConfig);
export const firestore = getFirestore(app);
export const auth = getAuth(app);

Вход в полноэкранный режим Выход из полноэкранного режима

Затем в моей логике аутентификации, при успешном входе/подписании, я автоматически подписываю пользователя в firebase, используя токен, полученный с сервера django.
Подробности смотрите здесь

import { auth as firebaseAuth } from "./firebase";

const initializeFirebaseAuth = async (user: MeResponse) => {
    return signInWithCustomToken(firebaseAuth, user.fb_token);
};

Вход в полноэкранный режим Выход из полноэкранного режима

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


import React, { createContext, useEffect, useState, useContext } from "react";
import { doc, onSnapshot, deleteDoc, collection } from "firebase/firestore";
import { firestore } from "../firebase";
import { useAuth } from "../hooks/useAuth";

interface NotificationPayload {
  message: string;
  id: string;
}

interface NotificationState {
  messages: NotificationPayload[];
  unreadCount: number;
  resetUnreadCount: () => void;
  markAllAsRead: () => void;
  markOneMessageAsRead: (id: string) => void;
}

export const NotificationsContext = createContext<NotificationState>({
  messages: [],
  unreadCount: 0,
  resetUnreadCount: () => {},
  markAllAsRead: () => {},
  markOneMessageAsRead: () => {},
});

export const NotificationProvider: React.FC<React.PropsWithChildren> = ({
  children,
}) => {
  const [messages, setMessages] = useState<NotificationPayload[]>([]);
  const [unreadCount, setUnreadCount] = useState(0);
  const { isAuthenticated, user } = useAuth();

  useEffect(() => {
    let unsubscribe: () => void;
    if (isAuthenticated && user?.user) {
      unsubscribe = onSnapshot(
        collection(
          firestore,
          "app-notifications",
          user.user.id,
          "user-notifications"
        ),
        (snapshot) => {
          const messages = snapshot.docs.map((doc) => ({
            message: doc.data().message,
            id: doc.id,
          }));
          setMessages(messages);
          setUnreadCount((prev) => (messages.length ? prev + 1 : 0));
        }
      );
    }
    return () => {
      if (unsubscribe) {
        unsubscribe();
      }
    };
  }, [isAuthenticated, user]);

  const resetUnreadCount = () => setUnreadCount(0);

  const markAllAsRead = async () => {
    if (isAuthenticated && user?.user) {
      await Promise.all(
        messages.map(async (msg) => {
          await deleteDoc(
            doc(
              firestore,
              "app-notifications",
              user.user.id,
              "user-notifications",
              msg.id
            )
          );
        })
      );
      setMessages([]);
    }
  };

  const markOneMessageAsRead = async (id: string) => {
    if (isAuthenticated && user?.user) {
      await deleteDoc(
        doc(
          firestore,
          "app-notifications",
          user.user.id,
          "user-notifications",
          id
        )
      );
    }
    setMessages(messages.filter((msg) => msg.id !== id));
  };

  return (
    <NotificationsContext.Provider
      value={{
        messages,
        unreadCount,
        resetUnreadCount,
        markAllAsRead,
        markOneMessageAsRead,
      }}
    >
      {children}
    </NotificationsContext.Provider>
  );
};

export const useFirebaseNotifiactions = useContext(NotificationsContext);


Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Изучение этого нового способа реализации функций реального времени было захватывающим, и, без сомнения, это один из лучших вариантов, когда речь идет о таких критических факторах, как масштабируемость, удобство обслуживания и время реализации.
Если вы хотите протестировать его, вот демонстрация в реальном времени👇

https://django-firebase.vercel.app

Если вы хотите увидеть другой похожий подход с PubNub, прочитайте эту статью👇.

Как добавить уведомления в реальном времени с помощью Django и nextjs

Abdullah Adeel ・ Jul 25 ・ 5 min read

#django #pubnub #nextjs #python

Если вам понравилось то, что вы только что прочитали, почему бы не проследить за вами в Twitter abdadeel_

Спасибо 👋

Оцените статью
devanswers.ru
Добавить комментарий