チャット内部デバイス
図1 UMLシーケンス図

図1 UMLシーケンス図
実装
実装中、WebHookを使用する代わりに、展開を簡素化し、資料の理解を容易にするために、Telegramサーバーを定期的にポーリングすることが決定されました。
仮想環境の準備
virtualenvがない場合は、インストールする必要があります。
pip install virtualenv
仮想環境を作成します。
virtualenv --no-site-packages -p python3.4 chat
有効化する:
source chat/bin/activate
チャットが機能するために必要なすべてのライブラリをインストールします。
pip install tornado==4.4.2 psycopg2==2.7.3 pyTelegramBotAPI==2.2.3
サーバーをポーリングするには、 ライブラリを使用して電報を処理します。
次のファイル構造を作成する必要があります。

ボットの作成
ボットを作成する時が来ました。この実装は、複数のボットが複数のクライアントと並行して通信できるように設計されています。
ボットを登録するには、 BotFather / newbotを記述する必要があり、それとのダイアログですべての詳細な指示を受け取ります。 その結果、登録が成功すると、BotFatherは新しいボットのトークンを返します。
次に、ボットがメッセージの送信先を認識できるように、chat_idを取得する必要があります。
これを行うには、電報アプリケーションでボットを見つけ、/ startコマンドでやり取りを開始し、メッセージを書き込んでリンクをたどります-
https://api.telegram.org/bot<__>/getUpdates
おおよそ次の答えが表示されます-
{"id":555455667,"first_name":"","last_name":"","username":"kamrus","language_code":"ru-RU"}
id chat_id
Postgresのセットアップ
チャットの作業とその近代化の可能性に柔軟性を提供するには、データベースを使用する必要があります。私はpostgresを選択しました。
postgresユーザーに切り替えます。
sudo su - postgres
postgres CLIに入ります。
psql
新しいUnicodeエンコードデータベースを作成する必要があります。
CREATE DATABASE habr_chat ENCODING 'UNICODE';
データベースに新しいユーザーを作成します。
CREATE USER habr_user WITH PASSWORD '12345';
そして、私たちは彼に基地に対するすべての特権を与えます:
GRANT ALL PRIVILEGES ON DATABASE habr_chat TO habr_user;
作成したばかりのデータベースに接続しています:
\c habr_chat
ボットに情報を保存するためのテーブルを作成しましょう。次のモデルがあります。
物理モデル
図2チャットテーブルの物理モデル

図2チャットテーブルの物理モデル
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) )
また、ユーザーにテーブルに対するすべての特権を与えます。
GRANT ALL PRIVILEGES ON TABLE chat TO habr_user;
次に、ボットトークンを追加する必要があります。
INSERT INTO chat (token) VALUES ('your_bot_token');
CLIの終了:
\q
ユーザーを元に戻します:
exit
コード記述
まず、チャットの設定を別のファイルで行うようにします。
bot_settings.py
CHAT_ID = chat_id db = { 'db_name': 'habr_chat', 'user': 'habr_user', 'password': '12345', 'host': '', 'port': '' }
主な機能はcore.pyファイルにあります
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()
次に、静的ファイルを作成します。
chat.html
コードを表示
<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
コードを表示
.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; }
javascriptファイルを作成する前に、クライアントとマネージャーからのメッセージのコードがどのように見えるかを決定する必要があります。
クライアントからのメッセージのHTMLコード:
コードを表示
<div class="chatbox__body__message chatbox__body__message--right"> <img src="../static/user.png" alt=""> <p></p> </div>
マネージャーからのメッセージのHTMLコード:
コードを表示
<div class="chatbox__body__message chatbox__body__message--right"> <img src="../static/user.png" alt=""> <p></p> </div>
chat.js
コードを表示
(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>"); }
centos7での展開
まず、アプリケーションの仮想環境を構成する必要があります。実際には、実装ポイントのローカルマシンで既に行ったことを繰り返します。
環境をセットアップしたら、プロジェクトをそこに転送する必要があります。これを行う最も簡単な方法は、gitを使用することです。まずコードをリポジトリにアップロードし、そこからサーバーにクローンを作成する必要があります。
postgresをカスタマイズする
サーバーにpostgresがインストールされていない場合は、次のようにインストールできます。
sudo yum install postgresql-server postgresql-devel postgresql-contrib
postgresを実行します。
sudo postgresql-setup initdb
sudo systemctl start postgresql
自動起動を追加します。
sudo systemctl enable postgresql
次に、postgresユーザーでpsqlにアクセスし、ローカルマシンで行ったすべての操作を繰り返す必要があります。
バックグラウンドでスーパーバイザーを使用して竜巻アプリケーションを起動します。
まず、スーパーバイザーをインストールします。
sudo yum install supervisor
次に、/ 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
構成ファイルのパスを変更することを忘れないでください!
スーパーバイザーを開始する前に、フォルダーを作成する必要があります/ var / log / supervisord / tornadoログがその中に収集されるため、スーパーバイザーがtornado-8004を開始したがチャットが機能しない場合は、そこでエラーを探す必要があります。
スーパーバイザーを起動します。
sudo supervisorctl start tornado-8004
すべてが正常であることを確認します。
sudo supervisorctl status
このようなものを取得する必要があります:
tornado-8004 RUNNING pid 32139, uptime 0:08:10
ローカルマシンで、chat.jsを変更します。
var host = "ws://__:8084/ws";
そして、ブラウザでchat.htmlを開きます。
できた!
このようなチャットは、特別なジェスチャーなしでプロジェクトに固定できます。フィードバックを収集するために使用することも非常に便利です。