Пишем чат на Python и Django

Автор оригинала: Nick Parsons
  • Перевод
Добрый день, друзья. В преддверии старта курса «Web-разработчик на Python» традиционно делимся с вами полезным переводом.




Вы видите перед собой руководство, которое расскажет, как создать приложение-чат на Python, Django и React.

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

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

Содержание:

  • Интерфейс демо-чата на React
  • Установка Django/Python
  • Авторизация пользователей
  • Django Rest Framework
  • Генерация токенов для доступа к Stream-серверу чата
  • Интеграция авторизации в React
  • Отправка сообщений с сервера Python
  • Последние мысли

Github-репозиторий с кодом из статьи

Начнем!

Шаг 1: Интерфейс демо-чата на React


Прежде чем мы начнем думать о 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

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

$ python --version

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

$ pip install django
$ django-admin startproject mychat

И запустите приложение:

$ cd mychat
$ python manage.py runserver

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



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


Следующий шаг – настройка авторизации пользователей в Django.

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

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

Вы увидите вкладку администратора на подобие той, что ниже:



Шаг 4: Django Rest Framework


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

  • Регистрации пользователей;
  • Входа пользователей в систему.

Мы могли бы сделать их самостоятельно, однако существует пакет под названием 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. Итак, начнем.
Давайте немного упорядочим наши файлы и создадим папку приложения чата в нашем проекте (убедитесь, что вы находитесь в правильном каталоге):

$ 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: Интеграция авторизации в React


По очевидным причинам, добавление авторизации к фронтенду – это важный шаг. В нашем случае это особенно полезно, поскольку мы можем извлечь токен пользователя из 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 на валидный ID приложения Stream, который можно найти на дашборде.

Перезапустите приложение на фронтенде и вы увидите авторизацию! Введите адрес электронной почты и пароль, токен будет запрошен и сохранен в локальном хранилище.

Шаг 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

И вы увидите такой отклик:



Последние мысли


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

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

На этом все. До встречи на открытым вебинаре по теме Трюки Django ORM.
OTUS
Цифровые навыки от ведущих экспертов

Комментарии 3

    +4

    Название статьи на мой взгляд не отражает что сам чат во главе это Stream API. А Python, Django и React по сути лишь обвязка для рекламируемого сервиса.

      +3

      Я уж думал опять нерабочий синхронный велосипед для асинхронной задачи, а оказалось используется сторонний сервис. Это неспортивно

        +2

        Да уж, в статье просто наворачивают фреймворк на фреймворк, студент не поймет ни как работают сокеты, ни как работает авторизация.
        И потом у таких инженеров чат занимает 600мб ОЗУ (я про слак и скайп).

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое