Как сделать мессенджер на python
Перейти к содержимому

Как сделать мессенджер на python

  • автор:

Создание messengers бота на Python. Полное руководство

Функционал из данного гайда был перенесен на messengers Business API, с помощью которого вы также сможете производить рассылки, создавать чат боты и многое другое, но без риска быть заблокированным. В связи с изменением условий работы API, в данном гайде частично могут использоваться неактуальные сведения. Пожалуйста, посмотрите документацию перед началом работы. Сейчас Chat API предлагает самый доступный и автоматизированный messengers Business API на рынке с Многопользовательским Чатом, Визуальным конструктором ботов, готовыми интеграциями приложений и другими полезными функциями.

  • Вывод списка команд
  • Вывод ID текущего чата
  • Вывод текущего времени сервера, на котором работает бот.
  • Вывод вашего имени
  • Отправка файлов разных форматов (pdf, jpg, doc, mp3 и т.д.)
  • Отправка заранее записанных голосовых сообщений
  • Отправка гео-координат (локации)
  • Создание конференции (группы)

Внимание: чтобы бот работал, телефон должен быть всегда подключен к интернету и не должен использоваться для messengers Web

Подготовительная работа

В самом начале, сразу свяжем messengers с нашим скриптом, чтобы по мере написания кода — проверять его работу. Для этого переходим в личный кабинет и получаем там QR-код. Далее открываем messengers на мобильном телефоне, заходим в Настройки -> messengers Web -> Сканируем QR-код.

Теперь, чтобы сервер вызывал наш скрипт при новых сообщениях, нужно указать WebHook URL. WebHook URL – это ссылка, куда будут посылаться, методом POST, JSON–данные с информацией о входящих сообщениях или уведомлениях. Соответственно, чтобы бот работал нам требуется сервер – который эти данные будет принимать и обрабатывать. При написании этой статьи, мы развернули у себя сервер с помощью микро-фраемворка FLASK. Он позволяет удобно реагировать на входящие запросы и обрабатывать их.

Пишем инициализацию класса бота

Создадим файл “wabot.py” и в нем опишем класс для нашего бота. Нам потребуется импортировать данные библиотеки:

import json import requests import datetime 

Библиотека json отвечает за обработку формата Json. Requests – Нужен нам для того, чтобы обращаться к API сайта.

Далее создадим класс WABot

class WABot(): def __init__(self, json): self.json = json self.dict_messages = json['messages'] self.APIUrl = 'https://eu41.chat-api.com/instance12345/' self.token = 'abcdefg' 

В нем мы опишем конструктор класса, который по умолчанию будет принимать json – тот самый, что будет содержать информацию о входящих сообщениях (Его будет принимать Webhook и передавать в наш класс). Для того, чтобы узнать, как будет выглядеть принимаемый json – можно зайти в удобный раздел тестирования, который мы предоставляем в личном кабинете. В нем можно протестировать запросы и Webhook.

Здесь мы присваиваем атрибутам класса значения. Dict_messages – Словарь, который содержит информацию из сообщения в json файле, который мы приняли

self.json = json self.dict_messages = json['messages'] 

Посмотреть структуру json можно в разделе тестирования в элементе «Проверка WebHook». Требуется запустить проверку и отправлять в ваш messengers чат сообщения. На экране будет выводиться json, посылаемый к webhook.

self.APIUrl = 'https://eu41.chat-api.com/instance12345/' self.token = 'abcdefg' 

Вам требуется подставить свои данные APIUrl и token, их мы будем использовать для формирования запроса к API.

Пишем функционал бота

Отправка запросов

Для работы нам потребуется отправлять запросы к API. Напишем функцию, которая будет формировать запросы и отправлять их, исходя из наших параметров.

def send_requests(self, method, data): url = f"?token=" headers = answer = requests.post(url, data=json.dumps(data), headers=headers) return answer.json() 

send_requests — принимает два параметра: method и data.

  • method определяет, какой метод ChatAPI должен быть вызван.
  • data содержит необходимые для пересылки данные.

Подробнее о всех методах можно почитать в документации. Data – это словарь данных из которых мы сформируем json и передадим методом Post на сервер. (Т.к. для нашего функционала бота требуются лишь Post методы, только ими мы и будем пользоваться)

На данном этапе мы формируем строку запроса к API.

url = f"?token=" 

Далее обязательно нужно указать header Contet-Type и пометить его Application/Json, так как мы будем всегда передавать наши данные форматом json.

headers =  

Теперь формируем полноценный запрос с помощью requests.post и передаем на сервер api наши данные. Json.dump(data) – серриализует наш словарь data в формат json.

answer = requests.post(url, data=json.dumps(data), headers=headers) return answer.json() 

Возвращает функции ответ сервера в формате json.

Отправка сообщений

send_message — принимает два параметра: chatId и text.

  • chatId – Id чата, в который необходимо отправить сообщение
  • Text – Текст сообщения

Теперь напишем метод, который позволит отправлять сообщения в наш чат.

def send_message(self, chatId, text): data = answer = self.send_requests('sendMessage', data) return answer 

Формируем словарь data, который содержит в себе тело “chatId” — Id, куда требует отправить сообщение и тело ‘body’ с необходимым нам текстом.

data =

Далее передаем наши данные в метод, который мы написали в предыдущем этапе

answer = self.send_requests('sendMessage', data) return answer

Для того чтобы отправить сообщение в Chat Api используется метод «sendMessage», поэтому его мы и передаем в функции в качестве параметра вместе с нашим словарем data. И возвращаем ответ сервера.

Приветствие

Метод welcome будем вызывать по команде “hi” бота и при вводе несуществующей команды.

  • chatId – Id чата, в который необходимо отправить сообщение
  • noWelcome – Булева переменная, определяющая какой текст будет отправлен в чат: приветствие или список команд. По умолчанию False.
def welcome(self,chatId, noWelcome = False): welcome_string = '' if (noWelcome == False): welcome_string = "messengers Demo Bot Python\n" else: welcome_string = """Incorrect command Commands: 1. chatId - show ID of the current chat 2. time - show server time 3. me - show your nickname 4. file [format] - get a file. Available formats: doc/gif/jpg/png/pdf/mp3/mp4 5. ptt - get a voice message 6. geo - get a location 7. group - create a group with the bot""" return self.send_message(chatId, welcome_string)

Формируем нашу строку с сообщением исходя из переменной noWelcome и передаем в функцию send_message в качестве отправляемого текста.

Вывод chatId
def show_chat_id(self,chatId): return self.send_message(chatId, f"Chat ID : ")
Вывод времени
def time(self, chatId): t = datetime.datetime.now() time = t.strftime('%d:%m:%Y') return self.send_message(chatId, time)
Функция me

Выводит информацию о имени собеседника по команде ‘me’

def me(self, chatId, name): return self.send_message(chatId, name)
Функция file

Отправляет файл с указанным форматом в диалог

  • chatId – Id чата, в который необходимо отправить сообщение
  • format – формат файла, который необходимо отправить. Все отправляемые файлы хранятся на сервере.
def file(self, chatId, format): availableFiles = if format in availableFiles.keys(): data = < 'chatId' : chatId, 'body': f'https://domain.com/Python/', 'filename' : availableFiles[format], 'caption' : f'Get your file ' > return self.send_requests('sendFile', data)

Здесь мы сформировали словарь, который содержит в качестве ключей необходимые нам форматы, а в качестве значений имена файлов, которые лежат на сервере и ждут отправки:

availableFiles =

Далее проверяем существует ли в нашем словаре формат, который передал пользователь

Если существует – то формируем запрос на отправку файла, где:

if format in availableFiles.keys(): data = < 'chatId' : chatId, 'body': f'https://domain.com/Python/', 'filename' : availableFiles[format], 'caption' : f'Get your file ' > return self.send_requests('sendFile', data)
  • chatId – Id чата, в который необходимо отправить сообщение
  • Body – прямая ссылка до файла, который необходимо отправить
  • Filename – имя файла
  • Caption – текст, который будет отправлен вместе с файлом

Формируем запрос send_requests с параметром “sendFile” и передаем в него наши данные.

Функция ptt

Отправляет голосовое сообщение в диалог

def ptt(self, chatId): data = < "audio" : 'https://domain.com/Python/ptt.ogg', "chatId" : chatId >return self.send_requests('sendAudio', data)

Формируем словарь наших данных, где:

  • chatId – Id чата, в который необходимо отправить сообщение
  • audio – прямая ссылка на файл формата ogg

Отправляем запрос к api методом “sendAudio”

Функция geo
def geo(self, chatId): data = < "lat" : '51.51916', "lng" : '-0.139214', "address" :'Ваш адрес', "chatId" : chatId >answer = self.send_requests('sendLocation', data) return answer
  • chatId – Id чата, в который необходимо отправить сообщение
  • lat – заранее заданные координаты
  • lng – заранее заданные координаты
  • address – ваш адрес или любая необходимая вам строка.

После формирования словаря, отправляем запрос к API методом “sendLocation”

Функция group

Создает группу, в которой будете вы и бот

def group(self, author): phone = author.replace('@c.us', '') data = < "groupName" : 'Group with the bot Python', "phones" : phone, 'messageText' : 'It is your group. Enjoy' >answer = self.send_requests('group', data) return answer

author – тело json, посылаемое webhook, содержит информацию о том, кто отправил сообщение.

Данное тело содержит в себе информацию о номере пользователя, но с дополнительными символами. Вызовем функцию replace и удалим их, оставив лишь номер телефона:

phone = author.replace('@c.us', '')
data =
  • groupName – имя конференции после её создания
  • phones – телефоны необходимых участников конференции, можно передавать массив из нескольких телефонов
  • messageText – Первое сообщение в конференции

Отправляем запрос методом ‘group’

Обрабатываем запросы пользователей

Весь функционал нашего демо-бота мы описали, теперь требуется организовать логику работы бота, чтобы он мог реагировать на команды и взаимодействовать с пользователем. Для этого опишем еще одну функцию

def processing(self): if self.dict_messages != []: for message in self.dict_messages: text = message['body'].split() if not message['fromMe']: if text[0].lower() == 'hi': return self.welcome(id) elif text[0].lower() == 'time': return self.time(id) elif text[0].lower() == 'chatId': return self.show_chat_id(id) elif text[0].lower() == 'me': return self.me(id, message['senderName']) elif text[0].lower() == 'file': return self.file(id, text[1]) elif text[0].lower() == 'ptt': return self.ptt(id) elif text[0].lower() == 'geo': return self.geo(id) elif text[0].lower() == 'group': return self.group(message['author']) else: return self.welcome(id, True) else: return 'NoCommand'

Данную функцию будем вызывать каждый раз, когда будем получать данные в наш webhook.

Разберем её по порядку:

Помните атрибут нашего бота dict_messages, который мы создали в самом начале? Он содержит в себе словари сообщений, которые мы приняли. Данная проверка отсеивает данные, которые не содержат в себе сообщений. Так как к webhook может прийти запрос без сообщения.

if self.dict_messages != []:

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

for message in self.dict_messages: text = message['body'].split()

После вхождения в цикл мы объявляем переменную text – которая будет являться листом слов, содержащихся в нашем сообщении. Для этого мы обращаемся к словарю message по ключу [‘body’], чтобы получить текст входящего сообщения и просто вызываем функцию split(), которая позволит разбить текст на слова.

Далее мы делаем проверку, что входящее сообщение не от нас самих, посредством обращения к ключу ‘fromMe”, который содержит в себе True или False и проверяет от кого было сообщение.

Подготовили мануал о том, как написать вацап бота на питоне

Если же данной проверки не будет бот может уйти в бесконечную рекурсию.

Теперь мы получаем id чата из все того же самого словаря message по ключу [‘chatId’]. Обращаемся к первому элементу листа слов, приводим его в нижний регистр, чтобы бот мог реагировать на сообщения написанное КАПСОМ или ЗаБоРчИкОм и сравниваем его с необходимыми нам командами.

После сравнения просто вызываем функционал, который мы описали на предыдущих этапах с параметром id.

Поздравляю, наш бот готов!) Теперь мы можем полноценно реагировать на входящие сообщения.

Вам необходимо только подставить свой токен из личного кабинета и номер инстанса.

Сервер Flask

Для обработки запросов будем использовать сервер Flask. Создадим файл app.py, импортируем все необходимые библиотеки.

from flask import Flask, request, jsonify from wabot import WABot import json app = Flask(__name__) @app.route('/', methods=['POST']) def home(): if request.method == 'POST': bot = WABot(request.json) return bot.processing() if(__name__) == '__main__': app.run()

Инициализируем переменную app, которая будет являться классом Flask.

app = Flask(__name__) @app.route('/', methods=['POST']) def home(): if request.method == 'POST': bot = WABot(request.json) return bot.processing()

И пропишем для неё путь app.route(‘/’, methods = [‘POST’]). Данный декоратор означает, что наша функция home будет вызываться каждый раз, когда к нашему серверу flask будут обращаться посредством post запроса по главному пути.

Делаем проверку того, что к серверу обратились с помощью метода POST. Создаем экземпляр нашего бота и передаем в него json данные.

requests.json – позволяет получить json файлы из тела запроса, который был передан к нашему серверу.

def home(): if request.method == 'POST': bot = WABot(request.json) return bot.processing()

И теперь просто вызываем у нашего объекта метод bot.processing(), который отвечает за обработку запросов.

messengers бот на Python

Теперь необходимо загрузить наш сервер вместе с ботом на хостинг и в качестве webhook указать ваш домен. При каждом входящем сообщение на сервер будут приходить и обрабатываться данные.

Простой мессенджер на tkinter,socket и threading

В этой статье я бы хотел показать как написать простое приложение мессенджер менее чем в 150 строк.

Серверная часть

Начнём с сервера(наше приложение будет состоять из скриптов сервера и клиента), через который можно получать входящие запросы от клиентов, желающих общаться. Традиционно указываем путь до интерпретатора и импортируем необходимые модули. Конкретно socket и threading. Первый отвечает непосредственно за “общение” процесссов между собой, второй за многопоточность. О этих модулях подробно можно почитать например здесь — socket , threading.

Использование фреймворков, таких как Twisted и SocketServer, было возможным, но мне показалось это излишним для такого простого программного обеспечения, как наше.

#!/usr/bin/env python3 from socket import AF_INET, socket, SOCK_STREAM from threading import Thread 

Давайте обозначим константы, отвечающие например за адрес порта и размер буфера.

clients = <> addresses = <> HOST = '' PORT = 33000 BUFSIZ = 1024 ADDR = (HOST, PORT) SERVER = socket(AF_INET, SOCK_STREAM) SERVER.bind(ADDR) 

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

def accept_incoming_connections(): """Настраивает обработку для входящих клиентов.""" while True: client, client_address = SERVER.accept() print("%s:%s присоединился к переписке" % client_address) client.send(bytes("Привет!"+ "Введи своё имя и нажми Enter", "utf8")) addresses[client] = client_address Thread(target=handle_client, args=(client,)).start() 

Это просто цикл, который всегда ждет входящих соединений и, как только он его получает, регистрирует соединение (печатает некоторые сведения о соединении) и отправляет подключенному клиенту приветствие. Затем он сохраняет адрес клиента и позже запускает поток обработки для этого клиента. Конечно, мы еще не определили для этого целевую функцию handle_client (), но вот как мы это сделаем:

def handle_client(client): name = client.recv(BUFSIZ).decode("utf8") welcome = 'Добро пожаловать %s! если желаете покинуть чат то, нажмите  чтобы выйти.' % name client.send(bytes(welcome, "utf8")) msg = "%s Теперь в переписке" % name broadcast(bytes(msg, "utf8")) clients[client] = name while True: msg = client.recv(BUFSIZ) if msg != bytes(" ", "utf8"): broadcast(msg, name+": ") else: client.send(bytes(" ", "utf8")) client.close() del clients[client] broadcast(bytes("%s покинул переписку." % name, "utf8")) break 

Естественно, после того, как мы отправим новому клиенту приветственное сообщение, он ответит именем, которое он хочет использовать для дальнейшего общения. В функции handle_client () первая задача, которую мы делаем, — мы сохраняем это имя, а затем отправляем клиенту еще одно сообщение о дальнейших инструкциях. После этого идет основной цикл: здесь мы получаем дополнительные сообщения от клиента и, если сообщение не содержит инструкций для выхода, мы просто передаем сообщение другим подключенным клиентам (мы определим метод широковещания через минуту ). Если мы сталкиваемся с сообщением с инструкциями выхода (то есть клиент отправляет ), мы возвращаем то же самое сообщение клиенту, а затем мы закрываем сокет подключения для него. Затем мы делаем очистку, удаляя запись для клиента, и, наконец, сообщаем другим связанным людям, что этот конкретный человек покинул чат.

Теперь пропишем функцию broadcast ():

def broadcast(msg, prefix=""): for sock in clients: sock.send(bytes(prefix, "utf8")+msg) 

Эта функция просто отправляет сообщение всем подключенным клиентам и при необходимости добавляет дополнительный префикс. Мы передаем префикс для broadcast () в нашей функции handle_client () и делаем это так, чтобы люди могли точно знать, кто является отправителем конкретного сообщения. Это были все необходимые функции для нашего сервера. Наконец, мы добавили код для запуска нашего сервера и прослушивания входящих соединений:

if __name__ == "__main__": SERVER.listen(5) print("Ожидание соединения") ACCEPT_THREAD = Thread(target=accept_incoming_connections) ACCEPT_THREAD.start() # Бесконечный цикл. ACCEPT_THREAD.join() SERVER.close() 

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

В итоге получаем вот такой код для серверной части:

#!/usr/bin/env python3 from socket import AF_INET, socket, SOCK_STREAM from threading import Thread def accept_incoming_connections(): while True: client, client_address = SERVER.accept() print("%s:%s соединено" % client_address) client.send(bytes("Добро пожаловать , введите своё имя и нажмите Enter", "utf8")) addresses[client] = client_address Thread(target=handle_client, args=(client,)).start() def handle_client(client): name = client.recv(BUFSIZ).decode("utf8") welcome = 'Добро пожаловать %s! Если желаете выйти,то нажмите  чтобы выйти.' % name client.send(bytes(welcome, "utf8")) msg = "%s вступил в переписку" % name broadcast(bytes(msg, "utf8")) clients[client] = name while True: msg = client.recv(BUFSIZ) if msg != bytes(" ", "utf8"): broadcast(msg, name+": ") else: client.send(bytes(" ", "utf8")) client.close() del clients[client] broadcast(bytes("%s покинул переписку" % name, "utf8")) break def broadcast(msg, prefix=""): for sock in clients: sock.send(bytes(prefix, "utf8")+msg) clients = <> addresses = <> HOST = '' PORT = 33000 BUFSIZ = 1024 ADDR = (HOST, PORT) SERVER = socket(AF_INET, SOCK_STREAM) SERVER.bind(ADDR) if __name__ == "__main__": SERVER.listen(5) print("ожидание соединения") ACCEPT_THREAD = Thread(target=accept_incoming_connections) ACCEPT_THREAD.start() ACCEPT_THREAD.join() SERVER.close() 

Клиентская часть###

Теперь приступим к наиболее интересной части нашего приложения — к клиенту. В качестве gui будем использовать tkinter, т.к в нём довольно легко построить несложное приложение. Традиционно импортируем модуль tkinter, а также модули использовавшиеся ранее при написании серверной части программы.

#!/usr/bin/env python3 from socket import AF_INET, socket, SOCK_STREAM from threading import Thread import tkinter 

Теперь мы напишем функции для обработки отправки и получения сообщений. Начнем с получения:

def receive(): """обработка получения сообщений""" while True: try: msg = client_socket.recv(BUFSIZ).decode("utf8")# декодируем,чтобы не получить кракозябры msg_list.insert(tkinter.END, msg) except OSError: break 

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

Функциональность внутри цикла довольно проста; recv () является блокирующей частью. Он останавливает выполнение до тех пор, пока не получит сообщение, а когда это произойдет, мы продвигаемся вперед и добавляем сообщение в msglist. Затем мы определяем msg_list, который является функцией Tkinter для отображения списка сообщений на экране. Далее мы определим функцию send ():

def send(event=None): """обработка отправленных сообщений""" msg = my_msg.get() my_msg.set("") # очищаем поле. client_socket.send(bytes(msg, "utf8")) if msg == " ": client_socket.close() top.quit() 

my_msg — это поле ввода в графическом интерфейсе, и поэтому мы извлекаем сообщение для отправки с помощью msg = my_msg.get (). После этого мы очищаем поле ввода и затем отправляем сообщение на сервер, который, как мы видели ранее, передает это сообщение всем клиентам (если это не сообщение о выходе). Если это сообщение о выходе, мы закрываем сокет, а затем приложение с графическим интерфейсом (через top.close ())

Мы определяем еще одну функцию, которая будет вызываться, когда мы решим закрыть окно с GUI. Это своего рода функция очистки до закрытия, которая закрывает соединение с сокетом до закрытия графического интерфейса:

def on_closing(event=None): """Эта функция вызывается когда закрывается окно""" my_msg.set(" ") send() 

Это устанавливает в поле ввода значение , а затем вызывает send (). Начнем с определения виджета верхнего уровня и установки его заголовка, как и в любой другой программе на tkinter:

top = tkinter.Tk() top.title("TkMessenger") 

Затем создаём фрейм со списком сообщений, поле для ввода сообщений и скроллбар для перемещения по истории переписки

messages_frame = tkinter.Frame(top) my_msg = tkinter.StringVar() my_msg.set("Введите ваше сообщение здесь.") scrollbar = tkinter.Scrollbar(messages_frame)#скроллбар 

“Упаковываем” наши элементы и размечаем их расположение в окне:

msg_list = tkinter.Listbox(messages_frame, height=15, width=50, yscrollcommand=scrollbar.set) scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) msg_list.pack(side=tkinter.LEFT, fill=tkinter.BOTH) msg_list.pack() messages_frame.pack() 

После этого мы создаем поле ввода для пользователя, чтобы ввести свое сообщение, и привязать его к строковой переменной, определенной выше. Мы также привязываем его к функции send (), чтобы всякий раз, когда пользователь нажимает return, сообщение отправлялось на сервер.

Далее мы создаем кнопку отправки, если пользователь желает отправить свои сообщения, нажав на нее. Опять же, мы связываем нажатие этой кнопки с функцией send ().

И да, мы также упаковываем все то, что создали только сейчас. Кроме того, не забудьте использовать функцию очистки on_closing (), которая должна вызываться, когда пользователь хочет закрыть окно GUI:

entry_field = tkinter.Entry(top, textvariable=my_msg) entry_field.bind("", send) entry_field.pack() send_button = tkinter.Button(top, text="отправить", command=send) send_button.pack() top.protocol("WM_DELETE_WINDOW", on_closing) 

И вот мы подходим к завершению. Мы еще не написали код для подключения к серверу. Для этого мы должны запросить у пользователя адрес сервера. Я сделал это, просто используя input (), чтобы пользователь встретился с подсказкой командной строки, запрашивающей адрес хоста перед запуском окна с графическим интерфейсом. В будущем можно добавить виджет для этой цели. А пока вот так:

HOST = input('Введите хост: ') PORT = input('Введите порт: ') if not PORT: PORT = 33000 # Стандартный порт else: PORT = int(PORT) BUFSIZ = 1024 ADDR = (HOST, PORT) client_socket = socket(AF_INET, SOCK_STREAM) client_socket.connect(ADDR) 

Как только мы получаем адрес и создаем сокет для подключения к нему, мы запускаем поток для получения сообщений, а затем основной цикл для нашего приложения с графическим интерфейсом:

receive_thread = Thread(target=receive) receive_thread.start() tkinter.mainloop() 

Вот и всё! Теперь наш скрипт клиентской части выглядит вот так:

#!/usr/bin/env python3 from socket import AF_INET, socket, SOCK_STREAM from threading import Thread import tkinter def receive(): while True: try: msg = client_socket.recv(BUFSIZ).decode("utf8") msg_list.insert(tkinter.END, msg) except OSError: break def send(event=None): msg = my_msg.get() my_msg.set("") client_socket.send(bytes(msg, "utf8")) if msg == " ": client_socket.close() top.quit() def on_closing(event=None): my_msg.set(" ") send() top = tkinter.Tk() top.title("TkMessenger") messages_frame = tkinter.Frame(top) my_msg = tkinter.StringVar() my_msg.set("Введите ваше сообщение здесь") scrollbar = tkinter.Scrollbar(messages_frame) msg_list = tkinter.Listbox(messages_frame, height=15, width=50, yscrollcommand=scrollbar.set) scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y) msg_list.pack(side=tkinter.LEFT, fill=tkinter.BOTH) msg_list.pack() messages_frame.pack() entry_field = tkinter.Entry(top, textvariable=my_msg) entry_field.bind("", send) entry_field.pack() send_button = tkinter.Button(top, text="отправить", command=send) send_button.pack() top.protocol("WM_DELETE_WINDOW", on_closing) HOST = input('Введите хост: ') PORT = input('Введите порт: ') if not PORT: PORT = 33000 else: PORT = int(PORT) BUFSIZ = 1024 ADDR = (HOST, PORT) client_socket = socket(AF_INET, SOCK_STREAM) client_socket.connect(ADDR) receive_thread = Thread(target=receive) receive_thread.start() tkinter.mainloop() 

Да, наше приложение не может тягаться с такими гигантами как: telegram, viber, клиентами xmpp/jabber; однако нам удалось создать простой чат, который каждый может развить в что-то своё: сделать уклон в безопасность(например шифруя передаваемые пакеты) или в хороший ux/ui. Получилась своего рода база для чего-то большего и это круто. Спасибо за прочтение, буду рад любым замечаниям и пожеланиям. Традиционно исходный код программы доступен в моём репозитории на github.

Мессенджер на python

Появилась мысль создать простой чат\мессенджер на Python(+- 100 участников), в следствии чего возник вопрос. Мне придется искать сервер и платить за него, или можно это сделать как-то без него? А если можно без него, то как это реализовать? Попытки найти ответ на мой вопрос в интернете были, но либо я не понял, либо ответа на него не было. Если вы имеете представление как это сделать, прошу помочь, хотя бы книгой, роликом или статьей где будет про это говориться. (я не в коем случае не прошу готовое решение, я просто решил спросить у людей, которые понимают в программировании лучше меня)

Отслеживать
задан 22 июн 2021 в 22:30
193 1 1 серебряный знак 8 8 бронзовых знаков

Всё зависит от ваших целей, технически вам никто не мешает запустить свой мессенджер хоть на обычном телефоне, лишь бы ваши 100 участников могли подключиться к этому телефону

22 июн 2021 в 22:38

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Я пару раз выкладывал концепт p2p месенжера на стековерфлоу. работать можно без сервера вообще, но нужен ещё один месенжер чтоб обменяться контактами для соединения.

Реализуется это через udp и stun. stun пробивает udp порт во внешнюю сеть, а udp позволяет посылать сообщения без установки соединения = без сервера.

Нужно только опубликовать контакт (ip, port) одного из участников и через него получить остальные. Что-то в стиле DHT. Или использовать другой месенжер (например пуши гугла, публичный сип сервер или даже смски) для обмена контактами.

Если вы новичок в сетевом программировании — то начните с месенжера с сервером. Сервер можно найти бесплатно по акции или на начальных тарифах pythonanywhere, oracle и тп. На домашнем интернете есть шанс что у вас белый адрес — можно пробросить порт.

Пишем свой мессенджер P2P

На фоне обсуждения будущего интернет мессенджеров и прочтения статьи «Почему ваш любимый мессенджер должен умереть», решил поделиться своим опытом создания P2P приложения для общения независимо от сторонних серверов. Точнее — это просто заготовка, передающая одно сообщение от клиента серверу, дальнейшее расширение функционала зависит только от Вашей фантазии.

В этой публикации мы напишем 3 простых приложения для связи P2P из любой точки Земного шара — клиент, сервер и сигнальный сервер.

Нам понадобится:
— один сервер с белым статическим IP адресом;
— 2 компьютера за NAT с типом соединения Full Cone NAT (либо 1 компьютер с 2-мя виртуальными машинами);
— STUN-сервер.

Full Cone NAT — это такой тип преобразования сетевых адресов, при котором существует однозначная трансляция между парами «внутренний адрес: внутренний порт» и «публичный адрес: публичный порт».

Вот, что мы можем прочесть о STUN-сервере на Wiki:

«Существуют протоколы, использующие пакеты UDP для передачи голоса, изображения или текста по IP-сетям. К сожалению, если обе общающиеся стороны находятся за NAT’ом, соединение не может быть установлено обычным способом. Именно здесь STUN и оказывается полезным. Он позволяет клиенту, находящемуся за сервером трансляции адресов (или за несколькими такими серверами), определить свой внешний IP-адрес, способ трансляции адреса и порта во внешней сети, связанный с определённым внутренним номером порта.»

При решении задачи использовались следующие питоновские модули: socket, twisted, stun, sqlite3, os, sys.

Для обмена данными, как между Сервером и Клиентом, так и между Сервером, Клиентом и Сигнальным Сервером — используется UDP протокол.

В общих чертах механизм функционирования выглядит так:

Сервер STUN сервер
Клиент STUN сервер

Сервер Сигнальный Сервер
Клиент Сигнальный Сервер

1. Клиент, находясь за NAT с типом соединения Full Cone NAT, отправляет сообщение на STUN сервер, получает ответ в виде своего внешнего IP и открытого PORT;

2. Сервер, находясь за NAT с типом соединения Full Cone NAT, отправляет сообщение на STUN сервер, получает ответ в виде своего внешнего IP и открытого PORT;

При этом, Клиенту и Серверу известен внешний (белый) IP и PORT Сигнального Сервера;

3. Сервер отправляет на Сигнальный Сервер данные о своих внешних IP и PORT, Сигнальный Сервер их сохраняет;

4. Клиент отправляет на Сигнальный Сервер данные о своих внешних IP и PORT и id_destination искомого Сервера, для которого ожидает его внешний IP, PORT.

Сигнальный Сервер их сохраняет, осуществляет поиск по базе, используя id_destination и, в ответ, отдает найденную информацию в виде строки: ‘id_host, name_host, ip_host, port_host’;

5. Клиент принимает найденную информацию, разбивает по разделителю и, используя (ip_host, port_host), отправляет сообщение Серверу.

Приложения написаны на Python версии 2.7, протестированы под Debian 7.7.

Создадим файл server.py с содержимым:

server.py

# -*- coding: utf-8 -*- #SERVER from socket import * import sys import stun def sigserver_exch(): # СЕРВЕР СИГНАЛЬНЫЙ СЕРВЕР # СЕРВЕР '' udp_socket.close() sigserver_exch() 

Заполним соответствующие поля разделов: «Внешний IP и PORT СИГНАЛЬНОГО СЕРВЕРА» и «IP и PORT этого КЛИЕНТА».

Создадим файл client.py с содержимым:

client.py

# -*- coding: utf-8 -*- # CLIENT from socket import * import sys import stun def sigserver_exch(): # КЛИЕНТ СИГНАЛЬНЫЙ СЕРВЕР # КЛИЕНТ -> СЕРВЕР # КЛИЕНТ - отправляет запрос на СИГНАЛЬНЫЙ СЕРВЕР с белым IP # для получения текущих значений IP и PORT СЕРВЕРА за NAT для подключения к нему. #Внешний IP и PORT СИГНАЛЬНОГО СЕРВЕРА: v_sig_host = 'XX.XX.XX.XX' v_sig_port = XXXX #id этого КЛИЕНТА, имя этого КЛИЕНТА, id искомого СЕРВЕРА v_id_client = 'id_client_1001' v_name_client = 'name_client_1' v_id_server = 'id_server_1002' #IP и PORT этого КЛИЕНТА v_ip_localhost = 'XX.XX.XX.XX' v_port_localhost = XXXX udp_socket = '' try: #Получаем текущий внешний IP и PORT при помощи утилиты STUN nat_type, external_ip, external_port = stun.get_ip_info() #Присваиваем переменным белый IP и PORT сигнального сервера для отправки запроса host_sigserver = v_sig_host port_sigserver = v_sig_port addr_sigserv = (host_sigserver,port_sigserver) #Заполняем словарь данными для отправки на СИГНАЛЬНЫЙ СЕРВЕР: #текущий id + имя + текущий внешний IP и PORT, #и id_dest - id известного сервера с которым хотим связаться. #В качестве id можно использовать хеш случайного числа + соль data_out = v_id_client + ',' + v_name_client + ',' + external_ip + ',' + str(external_port) + ',' + v_id_server #Создадим сокет с атрибутами: #использовать пространство интернет адресов (AF_INET), #передавать данные в виде отдельных сообщений udp_socket = socket(AF_INET, SOCK_DGRAM) #Присвоим переменным свой локальный IP и свободный PORT для получения информации host = v_ip_localhost port = v_port_localhost addr = (host,port) #Свяжем сокет с локальными IP и PORT udp_socket.bind(addr) #Отправим сообщение на СИГНАЛЬНЫЙ СЕРВЕР udp_socket.sendto(data_out, addr_sigserv) while True: #Если первый элемент списка - 'sigserv' (сообщение от СИГНАЛЬНОГО СЕРВЕРА), #печатаем сообщение с полученными данными и отправляем сообщение #'Hello, SERVER!' на сервер по указанному в сообщении адресу. data_in = udp_socket.recvfrom(1024) data_0 = data_in[0] data_p = data_0.split(",") if data_p[0] == 'sigserv': print('signal server data: ', data_p) udp_socket.sendto('Hello, SERVER!',(data_p[3],int(data_p[4]))) else: print("No, it is not Rio de Janeiro!") udp_socket.close() except: print ('Exit!') sys.exit(1) finally: if udp_socket <> '' udp_socket.close() sigserver_exch() 

Заполним соответствующие поля разделов: «Внешний IP и PORT СИГНАЛЬНОГО СЕРВЕРА» и «IP и PORT этого КЛИЕНТА».

Создадим файл signal_server.py с содержимым:

signal_server.py

# -*- coding: utf-8 -*- # SIGNAL SERVER #Twisted - управляемая событиями(event) структура #Событиями управляют функции – event handler #Цикл обработки событий отслеживает события и запускает соответствующие event handler #Работа цикла лежит на объекте reactor из модуля twisted.internet from twisted.internet import reactor from twisted.internet.protocol import DatagramProtocol import sys, os import sqlite3 class Query_processing_server(DatagramProtocol): # СИГНАЛЬНЫЙ СЕРВЕР КЛИЕНТ # КЛИЕНТ -> СЕРВЕР # либо # СИГНАЛЬНЫЙ СЕРВЕР СЕРВЕР # СИГНАЛЬНЫЙ СЕРВЕР - принимает запросы от КЛИЕНТА и СЕРВЕРА # сохраняет их текущие значения IP и PORT # (если отсутствуют - создает новые + имя и идентификатор) # и выдает IP и PORT СЕРВЕРА запрошенного КЛИЕНТОМ. def datagramReceived(self, data, addr_out): conn = '' try: #Разбиваем полученные данные по разделителю (,) [id_host,name_host,external_ip,external_port,id_dest] #id_dest - искомый id сервера data = data.split(",") #Запрос на указание пути к файлу БД sqlite3, при отсутствии будет создана новая БД по указанному пути: path_to_db = raw_input('Enter name db. For example: "/home/user/new_db.db": ') path_to_db = os.path.join(path_to_db) #Создать соединение с БД conn = sqlite3.connect(path_to_db) #Преобразовывать байтстроку в юникод conn.text_factory = str #Создаем объект курсора c = conn.cursor() #Создаем таблицу соответствия для хостов c.execute('''CREATE TABLE IF NOT EXISTS compliance_table ("id_host" text UNIQUE, "name_host" text, "ip_host" text, \ "port_host" text)''') #Добавляем новый хост, если еще не создан #Обновляем данные ip, port для существующего хоста c.execute('INSERT OR IGNORE INTO compliance_table VALUES (?, ?, ?, ?);', data[0:len(data)-1]) #Сохраняем изменения conn.commit() c.execute('SELECT * FROM compliance_table') #Поиск данных о сервере по его id c.execute('''SELECT id_host, name_host, ip_host, port_host from compliance_table WHERE id_host=?''', (data[len(data)-1],)) cf = c.fetchone() if cf == None: print ('Server_id not found!') else: #transport.write - отправка сообщения с данными: id_host, name_host, ip_host, port_host и меткой sigserver lst = 'sigserv' + ',' + cf[0] + ',' + cf[1] + ',' + cf[2] + ',' + cf[3] self.transport.write(str(lst), addr_out) #Закрываем соединение conn.close() except: print ('Exit!') sys.exit(1) finally: if conn <> '' conn.close() reactor.listenUDP(9102, Query_processing_server()) print('reactor run!') reactor.run() 

Порядок запуска приложения следующий:
— signal_server.py
— server.py
— client.py

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *