📜 ⬆️ ⬇️

Detailed guide to creating and deploying chat on Tornado + Telegram

This solution is suitable for small projects, since the opportunity to simultaneously conduct a dialogue with several users is realized by creating a new chat bot, that is, the more bots there are, the more people will be able to contact you at one time.

Internal chat device
image
Fig.1 UML sequence diagram

Implementation


During the implementation, it was decided to periodically poll the Telegram server in order to simplify deployment and to facilitate understanding of the material, as an alternative, you can use WebHook.

Preparing a virtual environment


If you are not worth virtualenv, then you need to install it:

pip install virtualenv

Create a virtual environment:
')
virtualenv --no-site-packages -p python3.4 chat

Activate it:

source chat/bin/activate

Install all the necessary libraries for our chat:

pip install tornado==4.4.2 psycopg2==2.7.3 pyTelegramBotAPI==2.2.3

To poll the server we will use the library to work with the telegram.

You must create the following file structure:



Create bot


It's time to create a bot, this implementation is designed for several bots to provide the ability to communicate in parallel with several clients.

To register a bot, you need to write a BotFather / newbot and all further instructions you will receive in a dialogue with him. As a result, after successful registration BotFather will return you the token of your new bot.

Now you need to get your chat_id so that the bot knows who to send messages to.
To do this, we find our bot in the telegram application, start the interaction with it with the / start command, write some message to it and follow the link -

https://api.telegram.org/bot<__>/getUpdates

We see about the following answer -

{"id":555455667,"first_name":"","last_name":"","username":"kamrus","language_code":"ru-RU"}
id chat_id


Postgres setup


To provide flexibility in the work of the chat and the possibility of its modernization, it is necessary to use the database, I chose postgres.

Switch to postgres user:

sudo su - postgres

Logging into the postgres CLI:

psql

You need to create a new database in Unicode;

 CREATE DATABASE habr_chat ENCODING 'UNICODE'; 

Create a new user in the database:

 CREATE USER habr_user WITH PASSWORD '12345'; 

And give him all the privileges to the base:

 GRANT ALL PRIVILEGES ON DATABASE habr_chat TO habr_user; 

Connect to the database you just created:

\c habr_chat

Create a table to store information bots, it will have the following model:

Physical model

Fig.2 Physical model of the chat table

 CREATE TABLE chat ( id SERIAL NOT NULL PRIMARY KEY, token character varying(300) NOT NULL UNIQUE, ready BOOLEAN NOT NULL DEFAULT True, last_message TEXT, customer_asked BOOLEAN NOT NULL DEFAULT False, remote_ip character varying(100) ) 

And just give the user all the privileges on the table:

 GRANT ALL PRIVILEGES ON TABLE chat TO habr_user; 

Now you need to add bot tokens to it:

 INSERT INTO chat (token) VALUES ('your_bot_token'); 

Exit CLI:

\q

and change user back:

exit

Code writing


First of all, we will move the settings for the chat to a separate file.

bot_settings.py

 CHAT_ID =   chat_id db = { 'db_name': 'habr_chat', 'user': 'habr_user', 'password': '12345', 'host': '', 'port': '' } 

The main functions will be in the core.py file.

 from telebot import apihelper from bot_settings import db import psycopg2 import datetime def get_updates(token, conn, cur, offset=None, limit=None, timeout=20): '''     ''' json_updates = apihelper.get_updates(token, offset, limit, timeout) try: answer = json_updates[-1]['message']['text'] except IndexError: answer = '' #        ,  #        , #         if is_customer_asked(conn, cur, token): #    ,      #             if not is_last_message(conn, cur, token, answer): #          #    update_last_message(conn, cur, token, answer) return answer else: #      ,      #   ,      , #       update_last_message(conn, cur, token, answer) def send_message(token, chat_id, text): '''    ''' apihelper.send_message(token, chat_id, text) def connect_postgres(**kwargs): try: conn = psycopg2.connect(dbname=db['db_name'], user=db['user'], password=db['password'], host=db['host'], port=db['port']) except Exception as e: print(e, '    posqgres') raise e cur = conn.cursor() return conn, cur def update_last_message(conn, cur, token, message, **kwargs): '''   ,   ''' query = "UPDATE chat SET last_message = %s WHERE token = %s" data = [message, token] try: cur.execute(query, data) conn.commit() except Exception as e: print(e, '       %s' %message) raise e def add_remote_ip(conn, cur, token, ip): '''   ip   ''' query = "UPDATE chat SET remote_ip = %s WHERE token = %s" data = [ip, token] try: cur.execute(query, data) conn.commit() except Exception as e: print(e, '    ip ') raise e def delete_remote_ip(conn, cur, token): '''  ip       ''' query = "UPDATE chat SET remote_ip = %s WHERE token = %s" data = ['', token] try: cur.execute(query, data) conn.commit() except Exception as e: print(e, '    ip ') raise e def is_last_message(conn, cur, token, message, **kwargs): '''         ''' query = "SELECT last_message FROM chat WHERE token = %s" data = [token, ] try: cur.execute(query, data) last_message = cur.fetchone() if last_message: if last_message[0] == message: return True return False except Exception as e: print(e, '    ') raise e def update_customer_asked(conn, cur, token, to_value): '''     ''' query = "UPDATE chat SET customer_asked = %s WHERE token = %s" # to_value = True/False data = [to_value, token] try: cur.execute(query, data) conn.commit() except Exception as e: print(e, '    "customer_asked"  %s' %to_value) raise e def is_customer_asked(conn, cur, token): '''     ,    True ''' query = "SELECT customer_asked FROM chat WHERE token = %s" data = [token, ] try: cur.execute(query, data) customer_asked = cur.fetchone() return customer_asked[0] except Exception as e: print(e, "          ") raise e def get_bot(conn, cur): '''      ,   ready = True.  (id, token, ready, last_message, customer_asked)    ''' query = "SELECT * FROM chat WHERE ready = True" try: cur.execute(query) bot = cur.fetchone() if bot: return bot else: return None except Exception as e: print(e, "     ") raise e def make_bot_busy(conn, cur, token): '''   ready  False,      ''' query = "UPDATE chat SET ready = False WHERE token = %s" data = [token,] try: cur.execute(query, data) conn.commit() except Exception as e: print(e, '     "ready"  False') raise e def make_bot_free(conn, cur, token): '''   ready  False,      ''' update_customer_asked(conn, cur, token, False) delete_remote_ip(conn, cur, token) query = "UPDATE chat SET ready = True WHERE token = %s" data = [token,] try: cur.execute(query, data) conn.commit() except Exception as e: print(e, '     "ready"  True') raise e 

tornadino.py

 import tornado.ioloop import tornado.web import tornado.websocket import core from bot_settings import CHAT_ID import datetime class WSHandler(tornado.websocket.WebSocketHandler): def __init__(self, application, request, **kwargs): super(WSHandler, self).__init__(application, request, **kwargs) #         postgres self.conn, self.cur = core.connect_postgres() self.get_bot(self.conn, self.cur, request.remote_ip) def get_bot(self, conn, cur, ip): while True: bot = core.get_bot(conn, cur) if bot: self.bot_token = bot[1] self.customer_asked = bot[4] #   core.make_bot_busy(self.conn, self.cur, self.bot_token) #   ip  core.add_remote_ip(self.conn, self.cur, self.bot_token, ip) break def check_origin(self, origin): '''       ''' return True def bot_callback(self): '''   PeriodicCallback    Telegram       ''' ans_telegram = core.get_updates(self.bot_token, self.conn, self.cur) if ans_telegram: #     ,       self.write_message(ans_telegram) def open(self): '''        ''' #    Telegram  3 self.telegram_loop = tornado.ioloop.PeriodicCallback(self.bot_callback, 3000) self.telegram_loop.start() def on_message(self, message): '''  ,      ''' if not self.customer_asked: self.customer_asked = True #    ,     core.update_customer_asked(self.conn, self.cur, self.bot_token, True) core.send_message(self.bot_token, CHAT_ID, message) def on_close(self): '''      ''' core.send_message(self.bot_token, CHAT_ID, "  ") #  PeriodicCallback self.telegram_loop.stop() #   core.make_bot_free(self.conn, self.cur, self.bot_token) # WebSocket     ws://127.0.0.1:8080/ws application = tornado.web.Application([ (r'/ws', WSHandler), ]) if __name__ == "__main__": application.listen(8080) tornado.ioloop.IOLoop.current().start() 

Now create a static file:
chat.html
View code
 <div class="chatbox chatbox-down chatbox--empty"> <div class="chatbox__title"> <h5><a href="#">Tornado-Telegram-chat</a></h5> <button class="chatbox__title__close"> <span> <svg viewBox="0 0 12 12" width="12px" height="12px"> <line stroke="#FFFFFF" x1="11.75" y1="0.25" x2="0.25" y2="11.75"></line> <line stroke="#FFFFFF" x1="11.75" y1="11.75" x2="0.25" y2="0.25"></line> </svg> </span> </button> </div> <div id="messages__box" class="chatbox__body"> <!--         --> </div> <button id="start-ws" type="button" class="btn btn-success btn-block"> </button> <form> <textarea id="message" class="chatbox__message" placeholder=" ..."></textarea> <input id="sendmessage" type="hidden"> </form> </div> 


chat.css
View code
 .chatbox { position: fixed; bottom: 0; right: 30px; height: 400px; background-color: #fff; font-family: Arial, sans-serif; -webkit-transition: all 600ms cubic-bezier(0.19, 1, 0.22, 1); transition: all 600ms cubic-bezier(0.19, 1, 0.22, 1); display: -webkit-flex; display: flex; -webkit-flex-direction: column; flex-direction: column; } .chatbox-down { bottom: -350px; } .chatbox--closed { bottom: -400px; } .chatbox .form-control:focus { border-color: #1f2836; } .chatbox__title, .chatbox__body { border-bottom: none; } .chatbox__title { min-height: 50px; padding-right: 10px; background-color: #1f2836; border-top-left-radius: 4px; border-top-right-radius: 4px; cursor: pointer; display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; } .chatbox__title h5 { height: 50px; margin: 0 0 0 15px; line-height: 50px; position: relative; padding-left: 20px; -webkit-flex-grow: 1; flex-grow: 1; } .chatbox__title h5 a { color: #fff; max-width: 195px; display: inline-block; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .chatbox__title h5:before { content: ''; display: block; position: absolute; top: 50%; left: 0; width: 12px; height: 12px; background: #4CAF50; border-radius: 6px; -webkit-transform: translateY(-50%); transform: translateY(-50%); } .chatbox__title__tray, .chatbox__title__close { width: 24px; height: 24px; outline: 0; border: none; background-color: transparent; opacity: 0.5; cursor: pointer; -webkit-transition: opacity 200ms; transition: opacity 200ms; } .chatbox__title__tray:hover, .chatbox__title__close:hover { opacity: 1; } .chatbox__title__tray span { width: 12px; height: 12px; display: inline-block; border-bottom: 2px solid #fff } .chatbox__title__close svg { vertical-align: middle; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.2px; } .chatbox__body, .chatbox__credentials { padding: 15px; border-top: 0; background-color: #f5f5f5; border-left: 1px solid #ddd; border-right: 1px solid #ddd; -webkit-flex-grow: 1; flex-grow: 1; } .chatbox__credentials { display: none; } .chatbox__credentials .form-control { -webkit-box-shadow: none; box-shadow: none; } .chatbox__body { overflow-y: auto; } .chatbox__body__message { position: relative; } .chatbox__body__message p { padding: 15px; border-radius: 4px; font-size: 14px; background-color: #fff; -webkit-box-shadow: 1px 1px rgba(100, 100, 100, 0.1); box-shadow: 1px 1px rgba(100, 100, 100, 0.1); } .chatbox__body__message img { width: 40px; height: 40px; border-radius: 4px; border: 2px solid #fcfcfc; position: absolute; top: 15px; } .chatbox__body__message--left p { margin-left: 15px; padding-left: 30px; text-align: left; } .chatbox__body__message--left img { left: -5px; } .chatbox__body__message--right p { margin-right: 15px; padding-right: 30px; text-align: right; } .chatbox__body__message--right img { right: -5px; } .chatbox__message { padding: 15px; min-height: 50px; outline: 0; resize: none; border: none; font-size: 12px; border: 1px solid #ddd; border-bottom: none; background-color: #fefefe; width: 100%; } .chatbox--empty { height: 262px; } .chatbox--empty.chatbox-down { bottom: -212px; } .chatbox--empty.chatbox--closed { bottom: -262px; } .chatbox--empty .chatbox__body, .chatbox--empty .chatbox__message { display: none; } .chatbox--empty .chatbox__credentials { display: block; } .description { font-family: Arial, sans-serif; font-size: 12px; } #start-ws { margin-top: 30px; } .no-visible { display: none; } 


Before writing a javascript file, you need to decide how the code will look for the message from the client and from the manager.

Html code for the message from the client:

View code
 <div class="chatbox__body__message chatbox__body__message--right"> <img src="../static/user.png" alt=""> <p></p> </div> 


Html code for the message from the manager:

View code
 <div class="chatbox__body__message chatbox__body__message--right"> <img src="../static/user.png" alt=""> <p></p> </div> 


chat.js
View code
 (function($) { $(document).ready(function() { var $chatbox = $('.chatbox'), $chatboxTitle = $('.chatbox__title'), $chatboxTitleClose = $('.chatbox__title__close'), $chatboxWs = $('#start-ws'); //         $chatboxTitle.on('click', function() { $chatbox.toggleClass('chatbox-down'); }); //   $chatboxTitleClose.on('click', function(e) { e.stopPropagation(); $chatbox.addClass('chatbox--closed'); //       ,  //    if (window.sock) { window.sock.close(); } }); //    $chatboxWs.on('click', function(e) { e.preventDefault(); //    $chatbox.removeClass('chatbox--empty'); //      $chatboxWs.addClass('no-visible'); if (!("WebSocket" in window)) { alert("    web sockets"); } else { alert(" "); setup(); } }); }); })(jQuery); //     WebSocket function setup(){ var host = "ws://62.109.2.175:8084/ws"; var socket = new WebSocket(host); window.sock = socket; var $txt = $("#message"); var $btnSend = $("#sendmessage"); //    textarea $txt.focus(); $btnSend.on('click',function(){ var text = $txt.val(); if(text == ""){return} //     socket.send(text); //     clientRequest(text); $txt.val(""); // $('#send') }); //   enter $txt.keypress(function(evt){ //    enter if(evt.which == 13){ $btnSend.click(); } }); if(socket){ //      socket.onopen = function(){ } //        socket.onmessage = function(msg){ //     managerResponse(msg.data); } //      socket.onclose = function(){ webSocketClose("The connection has been closed."); window.sock = false; } }else{ console.log("invalid socket"); } } function webSocketClose(txt){ var p = document.createElement('p'); p.innerHTML = txt; document.getElementById('messages__box').appendChild(p); } //    function clientRequest(txt) { $("#messages__box").append("<div class='chatbox__body__message chatbox__body__message--right'> <img src='../static/user.png' alt=''> <p>" + txt + "</p> </div>"); } //     function managerResponse(txt) { $("#messages__box").append("<div class='chatbox__body__message chatbox__body__message--left'> <img src='../static/user.png' alt=''> <p>" + txt + "</p> </div>"); } 


Deploy to centos7


First we need to set up a virtual environment for our application, in fact, repeat what we have already done on the local machine in the implementation clause.

After we set up the environment, we need to move our project there, the easiest way to do this is using git, you must first load the code into your repository and from there you can already clone it to the server.

Customize postgres


If you do not have postgres installed on your server, you can install it like this:

sudo yum install postgresql-server postgresql-devel postgresql-contrib

Run postgres:

sudo postgresql-setup initdb
sudo systemctl start postgresql

Add autorun:

sudo systemctl enable postgresql

After that, you need to go to psql under the postgres user and repeat everything we did on the local machine.

We will run our tornado application using the supervisor in the background.

First, install supervisor:

sudo yum install supervisor

Now open the configuration file of the supervisor, which will be located in /etc/supervisor.conf

[unix_http_server]
file=/path/to/supervisor.sock ; (the path to the socket file)

[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=error ; (log level;default info; others: debug,warn,trace)
pidfile=/path/to/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
user=root
childlogdir=/var/log/supervisord/ ; ('AUTO' child log dir, default $TEMP)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///path/to/supervisor.sock ; use a unix:// URL for a unix socket

[program:tornado-8004]
environment=PATH="/path/to/chat/bin"
command=/path/to/chat/bin/python3.4 /path/to/tornadino.py --port=8084
stopsignal=KILL
stderr_logfile=/var/log/supervisord/tornado-stderr.log
stdout_logfile=/var/log/supervisord/tornado-stdout.log

[include]
files = supervisord.d/*.ini


Do not forget to change the path in the configuration file!

Before you start a supervisor, you need to create a folder / var / log / supervisord / it will collect tornado logs, so if the supervisor launched tornado-8004, but the chat does not work, then you should look for the error there.

We start the supervisor:

sudo supervisorctl start tornado-8004

Check that everything is in order:

sudo supervisorctl status

Should get something like this:

tornado-8004 RUNNING pid 32139, uptime 0:08:10

On the local machine, we make changes to chat.js:

var host = "ws://__:8084/ws";

and open chat.html in the browser.

Done!

You can fasten such a chat to your projects without special gestures, it is also quite convenient to use to collect feedback.

Source: https://habr.com/ru/post/335660/


All Articles