Учебное пособие по Python Chat с Django и React

В этом уроке рассказывается о том, как построить чат-приложение с помощью Python, Django и React.

В отличие от других руководств, я не использую Python/Django для WebSocket-соединений. Хотя с технической точки зрения это может показаться круто, это довольно медлительно и дорого - особенно если у вас есть хоть сколько-нибудь приличное количество пользователей. Такие языки, как C++, Go и Elixir, гораздо лучше справляются с основной задачей чата.

В этом руководстве мы будем использовать Stream, API для чата, который позаботится о WebSocket-соединениях и другой тяжелой работе, используя Go, Raft и RocksDB.

Оглавление:

  1. React Chat Demo UI
  2. Настройка Django/Python
  3. User Auth
  4. Django Rest Framework
  5. Генерирование токенов для доступа к чат-серверу Stream
  6. Интеграция Auth в React
  7. Отправка сообщения с Python-сервера
  8. Заключительные мысли

Репо GitHub для приведенного ниже кода можно найти по адресу https://github.com/GetStream/python-chat-example.

Шаг 1 - React Chat Demo UI

Прежде чем мы начнем думать о чате на Python, давайте создадим простой фронтенд на React, чтобы у нас было на что посмотреть:

$ yarn global add create-react-app
$ brew install node && brew install yarn # skip if installed
$ create-react-app chat-frontend
$ cd chat-frontend
$ yarn add stream-chat-react

Замените код в src/App.js на:

import React from "react";
import {
  Chat,
  Channel,
  ChannelHeader,
  Thread,
  Window
} from "stream-chat-react";
import { MessageList, MessageInput } from "stream-chat-react";
import { StreamChat } from "stream-chat";

import "stream-chat-react/dist/css/index.css";

const chatClient = new StreamChat("qk4nn7rpcn75"); // Demo Stream Key
const userToken =
  "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1za3ktOSJ9.mhikC6HPqPKoCP4aHHfuH9dFgPQ2Fth5QoRAfolJjC4"; // Demo Stream Token

chatClient.setUser(
  {
    id: "cool-sky-9",
    name: "Cool sky",
    image: "https://getstream.io/random_svg/?id=cool-sky-9&name=Cool+sky"
  },
  userToken
);

const channel = chatClient.channel("messaging", "godevs", {
  // image and name are required, however, you can add custom fields
  image:
    "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png",
  name: "Talk about Go"
});

const App = () => (
  <Chat client={chatClient} theme={"messaging light"}>
    <Channel channel={channel}>
      <Window>
        <ChannelHeader />
        <MessageList />
        <MessageInput />
      </Window>
      <Thread />
    </Channel>
  </Chat>
);

export default App;

Далее выполните yarn start, чтобы увидеть чат в действии!

Шаг 2 - Настройка Django/Python (пропустить, если он у вас уже есть)

Убедитесь, что у вас установлен и работает Python 3.7.

$ brew install python3

$ pip install virtualenv virtualenvwrapper
$ export WORKON_HOME=~/Envs
$ source /usr/local/bin/virtualenvwrapper.sh
$ mkvirtualenv chatexample -p `which python3`
$ workon chatexample

Если это не помогло, попробуйте воспользоваться следующим фрагментом:

$ python3 -m venv chatexample
$ source chatexample/bin/activate

Теперь, когда вы находитесь в виртуальной среде env, вы должны увидеть python 3 при запуске:

$ python --version

Чтобы начать новый проект Django, используйте следующий фрагмент:

$ pip install django
$ django-admin startproject mychat

А для запуска приложения:

$ cd mychat
$ python manage.py runserver

Теперь, открыв http://localhost:8000, вы должны увидеть следующее:

Django

Шаг 3 - авторизация пользователя

В качестве следующего шага давайте настроим пользовательский аутентификатор Django.

$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py runserver

Посетите http://localhost:8000/admin/ и войдите в систему. Вуаля!

Вы должны увидеть экран администратора Django, как показано ниже:

Django Admin

Шаг 4 - Django Rest Framework

Одним из моих любимых пакетов для интеграции react с Django является Django Rest Framework. Для того чтобы все работало, нам потребуется создать конечные точки для:

  • User Signup
  • Вход пользователя

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

Для установки Djoser используйте следующий фрагмент:

$ pip install djangorestframework djoser

Затем отредактируйте urls.py и измените файл так, чтобы он содержал:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('auth/', include('djoser.urls')),
    path('auth/', include('djoser.urls.authtoken')),
]

После этого отредактируйте settings.py и внесите следующие изменения:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework.authtoken',
    'djoser',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
    )
}

Подробнее о конечных точках API, которые открывает Djoser, можно прочитать здесь:
https://djoser.readthedocs.io/en/latest/sample_usage.html

Теперь перейдем к тестированию конечной точки регистрации:

$ curl -X POST http://127.0.0.1:8000/auth/users/ --data 'username=djoser&password=alpine12'

Шаг 5 - Генерация токенов для доступа к чат-серверу Stream

Теперь нам необходимо настроить представления Djoser для генерации токенов для Stream. Давайте начнем.

Давайте немного упорядочим наши файлы и создадим папку chat app в нашем проекте (убедитесь, что вы находитесь в правильном каталоге):

$ python manage.py startapp auth

Установить stream-chat:

$ pip install stream-chat

Создайте в auth/serializers.py пользовательский сериализатор со следующей логикой:

from djoser.serializers import TokenSerializer
from rest_framework import serializers
from djoser.conf import settings as djoser_settings
from stream_chat import StreamChat
from django.conf import settings

class StreamTokenSerializer(TokenSerializer):
    stream_token = serializers.SerializerMethodField()

    class Meta:
        model = djoser_settings.TOKEN_MODEL
        fields = ('auth_token','stream_token')

    def get_stream_token(self, obj):
        client = StreamChat(api_key=settings.STREAM_API_KEY, api_secret=settings.STREAM_API_SECRET)
        token = client.create_token(obj.user.id)

        return token

И, наконец, используйте пользовательский сериализатор, обновив свой settings.py файл:

STREAM_API_KEY = YOUR_STREAM_API_KEY # https://getstream.io/dashboard/
STREAM_API_SECRET = YOUR_STREAM_API_SECRET
DJOSER = {
    'SERIALIZERS': {
        'token': 'auth.serializers.StreamTokenSerializer',
    }
}

Перезапуск миграции:

$ python manage.py migrate

Чтобы убедиться в его работоспособности, обратитесь к конечной точке входа в систему с POST-запросом:

$ curl -X POST http://127.0.0.1:8000/auth/token/login/ --data 'username=djoser&password=alpine12'

Должны быть возвращены и auth_token, и stream_token.

Шаг 6 - Интеграция Auth в React

Добавление auth позже во фронтенд является необходимым шагом по очевидным причинам. В нашем случае это особенно полезно, поскольку мы можем получить токен пользователя из API бэкенда (на основе Python) и динамически использовать его при отправке сообщений.

Сначала установите пакет промежуточного ПО CORS для Django:

$ pip install django-cors-headers

Затем измените свой settings.py так, чтобы он ссылался на промежуточное ПО djors-cors-header:

INSTALLED_APPS = (
    ...
    'corsheaders',
    ...
)

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]

И, наконец, добавьте в файл settings.py следующее:

CORS_ORIGIN_ALLOW_ALL = True

На следующем этапе необходимо внести несколько изменений во фронтенд. Для начала необходимо убедиться, что все зависимости установлены с помощью yarn:

$ yarn add axios react-dom react-router-dom

Далее создайте в каталоге src/ следующие файлы:

  • AuthedRoute.js
  • UnauthedRoute.js
  • withSession.js
  • Login.js
  • Chat.js

App.js

import React from "react";
import { BrowserRouter as Router, Switch } from "react-router-dom";

import Chat from "./Chat";
import Login from "./Login";

import UnauthedRoute from "./UnauthedRoute";
import AuthedRoute from "./AuthedRoute";

const App = () => (
  <Router>
    <Switch>
      <UnauthedRoute path="/auth/login" component={Login} />
      <AuthedRoute path="/" component={Chat} />
    </Switch>
  </Router>
);

export default App;

AuthedRoute.js

import React from "react";
import { Redirect, Route } from "react-router-dom";

const AuthedRoute = ({ component: Component, loading, ...rest }) => {
  const isAuthed = Boolean(localStorage.getItem("token"));
  return (
    <Route
      {...rest}
      render={props =>
        loading ? (
          <p>Loading...</p>
        ) : isAuthed ? (
          <Component history={props.history} {...rest} />
        ) : (
          <Redirect
            to={{
              pathname: "/auth/login",
              state: { next: props.location }
            }}
          />
        )
      }
    />
  );
};

export default AuthedRoute;

UnauthedRoute.js

import React from "react";
import { Redirect, Route } from "react-router-dom";

const AuthedRoute = ({ component: Component, loading, ...rest }) => {
  const isAuthed = Boolean(localStorage.getItem("token"));
  return (
    <Route
      {...rest}
      render={props =>
        loading ? (
          <p>Loading...</p>
        ) : !isAuthed ? (
          <Component history={props.history} {...rest} />
        ) : (
          <Redirect
            to={{
              pathname: "/"
            }}
          />
        )
      }
    />
  );
};

export default AuthedRoute;

withSession.js

import React from "react";
import { withRouter } from "react-router";

export default (Component, unAuthed = false) => {
  const WithSession = ({ user = {}, streamToken, ...props }) =>
    user.id || unAuthed ? (
      <Component
        userId={user.id}
        user={user}
        session={window.streamSession}
        {...props}
      />
    ) : (
      <Component {...props} />
    );

  return withRouter(WithSession);
};

Login.js

import React, { Component } from "react";
import axios from "axios";

class Login extends Component {
  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      email: "",
      password: ""
    };

    this.initStream = this.initStream.bind(this);
  }

  async initStream() {
    await this.setState({
      loading: true
    });

    const base = "http://localhost:8000";

    const formData = new FormData();
    formData.set("username", this.state.email);
    formData.set("password", this.state.password);

    const registration = await axios({
      method: "POST",
      url: `${base}/auth/users/`,
      data: formData,
      config: {
        headers: { "Content-Type": "multipart/form-data" }
      }
    });

    const authorization = await axios({
      method: "POST",
      url: `${base}/auth/token/login/`,
      data: formData,
      config: {
        headers: { "Content-Type": "multipart/form-data" }
      }
    });

    localStorage.setItem("token", authorization.data.stream_token);

    await this.setState({
      loading: false
    });

    this.props.history.push("/");
  }

  handleChange = e => {
    this.setState({
      [e.target.name]: e.target.value
    });
  };

  render() {
    return (
      <div className="login-root">
        <div className="login-card">
          <h4>Login</h4>
          <input
            type="text"
            placeholder="Email"
            name="email"
            onChange={e => this.handleChange(e)}
          />
          <input
            type="password"
            placeholder="Password"
            name="password"
            onChange={e => this.handleChange(e)}
          />
          <button onClick={this.initStream}>Submit</button>
        </div>
      </div>
    );
  }
}

export default Login;

Chat.js

import React, { Component } from "react";
import {
  Chat,
  Channel,
  ChannelHeader,
  Thread,
  Window
} from "stream-chat-react";
import { MessageList, MessageInput } from "stream-chat-react";
import { StreamChat } from "stream-chat";

import "stream-chat-react/dist/css/index.css";

class App extends Component {
  constructor(props) {
    super(props);
    this.client = new StreamChat("<YOUR_STREAM_APP_ID>");

    this.client.setUser(
      {
        id: "cool-sky-9",
        name: "Cool Sky",
        image: "https://getstream.io/random_svg/?id=cool-sky-9&name=Cool+sky"
      },
      localStorage.getItem("token")
    );

    this.channel = this.client.channel("messaging", "godevs", {
      image:
        "https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png",
      name: "Talk about Go"
    });
  }

  render() {
    return (
      <Chat client={this.client} theme={"messaging light"}>
        <Channel channel={this.channel}>
          <Window>
            <ChannelHeader />
            <MessageList />
            <MessageInput />
          </Window>
          <Thread />
        </Channel>
      </Chat>
    );
  }
}

export default App;

Обязательно замените YOUR_STREAM_APP_ID на действительный идентификатор Stream App ID, который можно найти на dashboard.

Перезапустите ваше фронтенд-приложение, и вы должны столкнуться с auth wall! Введите свой e-mail и пароль, после чего будет запрошен токен, который будет сохранен в локальном хранилище.

Шаг 7 - Отправка сообщения с чат-сервера Python

Иногда возникает необходимость писать в API чата, используя свой внутренний сервер на базе Python. Вот быстрая команда управления, которую вы можете использовать:

Убедитесь, что установленные приложения выглядят следующим образом в settings.py:

INSTALLED_APPS = [
    'corsheaders',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework.authtoken',
    'djoser',
]

Далее создайте каталог chat/management/commands. В этот каталог добавьте файл с именем broadcast.py с таким содержанием:

from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from stream_chat import StreamChat

class Command(BaseCommand):
    help = 'Broadcast the message on your channel'

    def add_arguments(self, parser):
        parser.add_argument('--message')

    def handle(self, *args, **options):
        client = StreamChat(api_key=settings.STREAM_API_KEY, api_secret=settings.STREAM_API_SECRET)
        client.update_user({"id": "system", "name": "The Server"})
        channel = client.channel("messaging", "kung-fu")
        channel.create("system")
        response = channel.send_message({"text": "AMA about kung-fu"}, 'system')
        self.stdout.write(self.style.SUCCESS('Successfully posted a message with id "%s"' % response['message']['id']))

Вы можете попробовать отправить сообщение в чат следующим образом:

$ python manage.py broadcast --message hello

И вы должны увидеть ответ следующего содержания:

Broadcast

Заключительные мысли

Надеюсь, вам понравилось это руководство по созданию чат-приложения с помощью Django, Python и React!

Для получения интерактивного представления о Stream Chat ознакомьтесь с нашим API Tutorial на сайте Stream. Если вам интересно покопаться в коде React-компонентов Stream Chat, полную документацию можно найти здесь. Если вы заинтересованы в создании чата на основе Stream, мы предлагаем различные SDK для популярных языков и фреймворков, в том числе для iOS (Swift).

Счастливого кодирования! ✌

Вернуться на верх