Написання бота для месенджера Tox

На тлі загальної захопленості створенням ботів для Telegram я б хотів розповісти про API не дуже широко відомого месенджера Tox і показати на прикладі простого echo-бота, як можна так само легко і швидко створювати власні.

image


Введення
Ядро Tox (toxcore на github) складається з трьох частин:

  • ToxCore — власне, саме ядро, яке дозволяє керувати контактами, повідомленнями, файлами, аватарами, статусами і груповими чатами.
  • ToxAV — підсистема голосових та відео-дзвінки.
  • ToxDNS — підсистема отримання ToxID з людино-читаного адреси (приблизного аналога email або JabberID) через запит до DNS.
Одиницею контакту є ToxID (він же «address» в термінах API) — шістнадцяткова рядок довжиною 76 символів, що кодує:

  • Публічний ключ (32 або 64 байта (символу).
  • Захист від спаму «nospam» (4 байта або 8 символів) — випадковий набір даних, зміна якого дозволяє продовжувати підтримувати авторизовані раніше контакти, але ігнорувати запити від нових.
  • Контрольна сума (2 байта або 4 символи) — операція XOR над публічним ключем і значенням «nospam», служить для швидкої перевірки коректності ToxID.
Загальний цикл роботи з Tox API можна уявити послідовно у вигляді:

  1. Ініціалізація ядра (tox_new) — тут виробляється установка протоколів (IPv4/IPv6, UDP/TCP), параметрів проксі (HTTP/SOCKS5), діапазонів використовуваних портів та (за наявності) завантаження раніше збереженого стану зі списком контактів.
  2. Установка callback функцій для обробки подій(tox_callback_*) — обробники подій викликаються з основного циклу (4) і у них зазвичай зосереджена основна логіка роботи програми.
  3. Підключення до однієї і більше DHT-нод(tox_bootstrap).
  4. Основний робочий цикл (tox_iterate) і обробка подій.
  5. Пауза на час tox_iteration_interval і повернення до попереднього кроку.
Оскільки основним способом отримання знань про роботу Tox API є читання вихідного коду (написаного на сі), для спрощення подальшого викладу я скористаюся обгорткою для мови python (pytoxcore на github). Для тих, хто не бажає займатися самостійною збіркою бібліотеки з исходников, там же є посилання на готові бінарні пакети для поширених дистрибутивів.

При використанні python-обгортки отримати довідку по бібліотеці можна наступним способом:

$ python
>>> from pytoxcore import ToxCore
>>> help(ToxCore)
class ToxCore(object)
| ToxCore object
...
| tox_add_tcp_relay(...)
| tox_add_tcp_relay(address, port, public_key)
| Adds additional host:port as pair TCP relay.
| This function can be used to initiate TCP connections to different ports on the same bootstrap node, or to add TCP relays without using them as bootstrap nodes.
| 
| tox_bootstrap(...)
| tox_bootstrap(address, port, public_key)
| Sends a "get nodes" request to the given bootstrap node with IP port, and public key to setup connections.
| This function will attempt to connect to the node using UDP. You must use this function even if Tox_Options.udp_enabled was set to false.
...

Нижче зупинимося на кожному кроці роботи з API трохи більш докладно.

Ініціалізація ядра
Для ініціалізації ядра в якості параметра використовується структура Tox_Options. В python це може бути словник з однойменними полями. Значення за замовчуванням можна отримати за допомогою виклику методуtox_options_default:

$ python
>>> from pytoxcore import ToxCore
>>> ToxCore.tox_options_default()
{'start_port': 0L, 'proxy_host': None, 'tcp_port': 0L, 'end_port': 0L, 'udp_enabled': True, 'savedata_data': None, 'proxy_port': 0L, 'ipv6_enabled': True, 'proxy_type': 0L}

Тут:

  • ipv6_enabled True або False залежно від того, чи ви є щасливим володарем IPv6.
  • udp_enabled — за винятком випадків роботи через проксі рекомендується встановлювати в значення True, т. к. UDP є «рідним» протоколом для Tox.
  • proxy_type — тип проксі, може приймати значення:
    • TOX_PROXY_TYPE_NONE — не використовувати проксі.
    • TOX_PROXY_TYPE_HTTP — HTTP проксі.
    • TOX_PROXY_TYPE_SOCKS5 — SOCKS5 проксі (наприклад, Tor).
  • proxy_host — хоста або IP-адресу проксі-сервера.
  • proxy_port — порт проксі-сервера (ігнорується для TOX_PROXY_TYPE_NONE).
  • start_port — початковий порт з діапазону дозволених портів.
  • end_port — кінцевий порт з діапазону дозволених портів. Якщо початковий та кінцевий порти дорівнюють 0, то використовується діапазон [33445, 33545]. Якщо тільки один з портів дорівнює нулю, то використовується єдиний ненульовий порт. У разі start_port > end_port, вони будуть поміняні місцями.
  • tcp_port — порт для підняття TCP сервера (релея), при значенні 0 сервер буде відключений. TCP релей дозволяє іншим користувачам використовувати ваш примірник як проміжний вузол (концепція super-node).
  • savedata_data — дані для завантаження або None у разі їх відсутності.
У переважній більшості випадків, всі параметри за винятком savedata_data можна залишити за замовчуванням:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
from pytoxcore import ToxCore

class EchoBot(ToxCore):
def __init__(self):
tox_options = ToxCore.tox_options_default()
if os.path.isfile("tox.save"):
with open("tox.save", "rb") as f:
tox_options["savedata_data"] = f.read()

super(EchoBot, self).__init__(tox_options)

При першому запуску ядро згенерує публічний і приватний ключі, які при необхідності можна зберегти:

class EchoBot(ToxCore):
...
def save_data(self):
with open("tox.save", "wb") as f:
f.write(self.tox_get_savedata())

Оскільки дані завжди містяться в пам'яті, то для захисту від випадкових збоїв я рекомендую періодично зберігати дані в тимчасовий файл і (в разі успіху і атомарно) замінювати їм основний файл з даними.

Поточні значення адреси для передачі користувачам, публічного і приватного ключів і значення «nospam» можна отримати викликами:

  • tox_self_get_address — поточний адреса (ToxID).
  • tox_self_get_public_key — публічний ключ.
  • tox_self_get_secret_key — приватний ключ.
  • tox_self_get_nospam / tox_self_set_nospam — отримання та встановлення значення «nospam».
$ python
>>> from pytoxcore import ToxCore
>>> core = ToxCore(ToxCore.tox_options_default())
>>> core.tox_self_get_address()
'366EA3B25BA31E3ADC4C476098A8686E4EAE87B04E4E4A3A3A0B865CBB9725704189FEDAEB26'
>>> core.tox_self_get_public_key()
'366EA3B25BA31E3ADC4C476098A8686E4EAE87B04E4E4A3A3A0B865CBB972570'
>>> core.tox_self_get_secret_key()
'86003764B4C99395E164024A17DCD0ECB80363C5976FF43ECE11637FA0B683F9'
>>> core.tox_self_get_nospam()
'4189FEDA'

Після ініціалізації ядра, боту в будь-який час можна встановити нік і підпис викликами tox_self_set_name та tox_self_set_status_message. Довжина імені не повинна перевищувати значення TOX_MAX_NAME_LENGTH, довжина підпису не повинна перевищувати значення TOX_MAX_STATUS_MESSAGE_LENGTH (розміри в байтах). Установка аватара буде розглянута нижче, т. к. технічно є відправкою файлу контакту.

Установка callback функцій
В python-обгортці підключення до підтримуваних callback функцій проводиться автоматично. Самі ж розробники можуть бути методами спадкоємця ToxCore і мають суфікс*_cb:

class EchoBot(ToxCore):
...
def tox_self_connection_status_cb(self, connection_status):
if connection_status == ToxCore.TOX_CONNECTION_NONE:
print("Disconnected from DHT")
elif connection_status == ToxCore.TOX_CONNECTION_TCP:
print("Connected to DHT via TCP")
elif connection_status == ToxCore.TOX_CONNECTION_UDP:
print("Connected to DHT via UDP")
else:
raise NotImplementedError("Unknown connection_status:{0}".format(connection_status))

Конкретні обробники та їх аргументи будуть розглянуті нижче.

Підключення до DHT-ноде
DHT-нода визначається IP адресою, номером порту і публічним ключем. Початковий список DHT-нсд можна взяти вікі проекту.

Згідно Tox Client Guidelines, клієнт кожні 5 секунд повинен намагатися підключитися як мінімум до чотирьох випадковим нодам, поки ядро не повідомить про успішне з'єднанні (див. tox_self_connection_status_cb). У разі завантаження з файлу стану клієнт не повинен намагатися з'єднуватися протягом 10 секунд після першого виклику tox_iterate і, в разі відсутності з'єднання, повторити агресивну стратегію з'єднання вище.

Для бота, який планує завжди бути на зв'язку, дані рекомендації виглядають трохи переусложненными. Можна спробувати скоротити необхідну кількість DHT-нод для підключення за рахунок підняття власної локальної DHT-ноди. Додатковий плюс наявності локальної ноди, окрім постійного зв'язку з нею, це «посилення» самої мережі Tox.

Для підняття локальної ноди буде потрібно встановити і налаштувати демонаtox-bootstrapd. Його можна зібрати разом з бібліотекою toxcore, а так само отримати в бінарному вигляді з репозиторію розробників.

Конфігурація демона задається у файлі/etc/tox-bootstrapd.conf і добре документована. Додаткову інформацію по запуску демона можна отримати у відповідному README, а для deb-дистрибутивів проекту tox.pkg установка пакета tox-bootstrapd самодостатня. Публічний ключ локальної DHT-ноди можна буде дізнатися в системному балці після запуску демона.

Т. о., спрощена версія з'єднання з DHT-нодой і робочого циклу може бути представлена у вигляді:

class EchoBot(ToxCore):
...
def run(self):
checked = False
self.tox_bootstrap("127.0.0.1", 33445,"366EA...72570")

while True:
status = self.tox_self_get_connection_status()
if not checked and status != ToxCore.TOX_CONNECTION_NONE:
checked = True
if checked and status == ToxCore.TOX_CONNECTION_NONE:
self.tox_bootstrap("127.0.0.1", 33445,"366EA...72570")
checked = False

self.tox_iterate()

interval = self.tox_iteration_interval()
time.sleep(interval / 1000.0)

У деяких випадках клієнт може працювати взагалі без виклику tox_bootstrap — для цього необхідно, щоб в рамках одного широкомовного домену мережі був запущений інший клієнт або DHT-нода. Ця особливість робить можливим спілкування всередині локальної мережі без необхідності виходу в інтернет і спілкування з зовнішнім світом, якщо хоча б один клієнт має доступ в мережу і є релеем.

Обробка подій
Як було написано раніше, для підключення обробника подій в python досить додати потрібний метод, з необхідними аргументами в клас-спадкоємець і в якості прикладу було наведено обробник стану з'єднання tox_self_connection_status_cb, який викликається при підключенні і відключенні бота від DHT-мережі.

Робота з контактами
Для того, щоб бот міг взаємодіяти з іншими учасниками мережі, вони повинні бути додані в контакт-лист бота. У термінах API контакт називається «friend» і позначається цілим числом("friend_number")унікальним протягом життя примірника ToxCore.

Коли клієнт робить запит на додавання бота в контакт лист, на стороні бота викликається обробник tox_friend_request_cb(public_key, message), де:

  • public_key — публічний ключ одного.
  • message — повідомлення від одного типу «Привіт, це abbat! Додасте мене в друзі?».
Всередині обробника ви можете додати людини в друзі викликавши tox_friend_add_norequest, так і просто проігнорувати запит. Всі подальші обробники подій будуть використовувати friend_number в якості ідентифікатора друга, який можна отримати з публічного ключа викликомtox_friend_by_public_key.

Після додавання контакту в друзі на стороні бота можуть виникати наступні події:

  • tox_friend_connection_status_cb(friend_number, connection_status) — зміна стану з'єднання одного, де connection_status може приймати значення:
    • TOX_CONNECTION_NONE — один в offline.
    • TOX_CONNECTION_TCP — один в online і з'єднаний по TCP.
    • TOX_CONNECTION_UDP — один в online і з'єднаний по UDP.
  • tox_friend_name_cb(friend_number, name) — зміна ніка одного.
  • tox_friend_status_message_cb(friend_number, message) — зміна підпису.
  • tox_friend_status_cb(friend_number, status) — зміна стану, де status може приймати значення:
    • TOX_USER_STATUS_NONE — доступний («online»).
    • TOX_USER_STATUS_AWAY — відійшов.
    • TOX_USER_STATUS_BUSY — зайнятий.
  • tox_friend_message_cb(friend_number, message) — повідомлення від одного.
  • tox_friend_read_receipt_cb(friend_number, message_id) — квитанція про одержання іншому повідомлення, відправленого викликом tox_friend_send_message (див. нижче).
У разі echo-бота при отриманні повідомлення від одного, його потрібно просто відправити назад:

class EchoBot(ToxCore):
...
def tox_friend_request_cb(self, public_key, message):
self.tox_friend_add_norequest(public_key)

def tox_friend_message_cb(self, friend_number, message):
message_id = self.tox_friend_send_message(friend_number, ToxCore.TOX_MESSAGE_TYPE_NORMAL, message)

Відправлення повідомлення одному здійснюється викликом методу tox_friend_send_message, який повертає ідентифікатор повідомлення message_id — монотонно зростаюча кількість, унікальне для кожного друга. В якості параметрів метод приймає ідентифікатор одного, тип повідомлення і сам текст повідомлення. На текст повідомлення накладаються наступні обмеження:

  • Повідомлення не може бути порожнім.
  • Повідомлення не може бути більше TOX_MAX_MESSAGE_LENGTH (байт), довгі повідомлення необхідно розбивати на частини.
У разі, якщо обробка повідомлення від одного вимагає деякого часу, поведінка бота можна урізноманітнити передаючи випадковим чином події «набору повідомлення»(tox_self_set_typing).

За значенням friend_number друга в будь-який момент можна отримати наступну інформацію про нього:

  • tox_friend_get_connection_status — поточний мережевий статус одного (останнє значенняtox_friend_connection_status_cb).
  • tox_friend_get_name — поточний нік одного (останнє значенняtox_friend_name_cb).
  • tox_friend_get_status_message — поточна підпис одного (останнє значенняtox_friend_status_message_cb).
  • tox_friend_get_status — поточний статус одного (останнє значенняtox_friend_status_cb).
  • tox_friend_get_last_online — дата останнього відвідування online (unixtime).
Додаткові операції з друзями:

  • tox_self_get_friend_list_size — отримання кількості друзів.
  • tox_self_get_friend_list — отримання списку friend_number друзів.
  • tox_friend_delete — видалення одного з списку контактів.
Тут здається все просто і інтуїтивно. Далі буде трохи складніше.

Робота з файлами
Прийом файлів
Коли хто-небудь з друзів відправляє боту файл, виникає подія tox_file_recv_cb(friend_number, file_number, kind, file_size, filename), де:

  • friend_number — номер одного (див. «Робота з контактами»).
  • file_number — номер файлу, унікальне число в рамках поточного списку прийнятих і переданих файлів від одного.
  • kind — тип файлу:
    • TOX_FILE_KIND_DATA — передаваний файл є простим файлом.
    • TOX_FILE_KIND_AVATAR — передаваний файл є аватаром одного.
  • file_size — розмір файлу. Для TOX_FILE_KIND_AVATAR розмір файлу рівний 0 означає, що в одного не встановлено аватар. Розмір файлу рівний UINT64_MAX позначає невідомий розмір файлу (потоком).
  • filename — ім'я файлу.
Тут слід звернути особливу увагу на параметр filename. Незважаючи на те, що специфікація вимагає передавати всі дані в UTF-8 і ім'я файлу не може містити частин шляху, в реальному житті може прилетіти все що завгодно аж до нечитаних бінарних даних, що містять символи переведення рядка і нулі.

При виникненні цієї події наступним дією бота повинен бути виклик керуючого методу tox_file_control(friend_number, file_number, control), де:

  • friend_number — номер друга.
  • file_number — номер файлу.
  • control — команда управління файлом:
    • TOX_FILE_CONTROL_CANCEL — відмінити прийом файлу.
    • TOX_FILE_CONTROL_PAUSE — поставити передачу файлу на паузу (підтримується не всіма клієнтами).
    • TOX_FILE_CONTROL_RESUME — продовжити передачу файлу.
Для echo-бота прийом файлів не потрібно, він завжди може скасовувати операцію:

class EchoBot(ToxCore):
...
def tox_file_recv_cb(self, friend_number, file_number, kind, file_size, filename):
self.tox_file_control(friend_number, file_number, ToxCore.TOX_FILE_CONTROL_CANCEL)

У разі ж передачі управління через TOX_FILE_CONTROL_RESUME, починає викликати подія tox_file_recv_chunk_cb(friend_number, file_number, position, data), де:

  • friend_number — номер друга.
  • file_number — номер файлу.
  • position — поточна позиція у файлі.
  • data — чанк даних або None для кінця передачі.
Тут слід звернути увагу на те, що position не зобов'язана монотонно зростати — в загальному випадку чанкі можуть приходити в будь-якій послідовності і будь-якої довжини.

Передача файлів
Для початку процедури передачі файлу необхідний виклик методу tox_file_send(friend_number, kind, file_size, file_id, filename), де:

  • friend_number — номер друга.
  • kind значення TOX_FILE_KIND_DATA абоTOX_FILE_KIND_AVATAR.
  • file_size — розмір файлу (спеціальні значення 0 і UINT64_MAX розглянуті вище).
  • file_id — унікальний ідентифікатор файла завдовжки TOX_FILE_ID_LENGTH, який дозволяє продовжити передачу після рестарту ядра або None для автоматичної генерації.
  • filename — ім'я файлу.
Тут особливим параметром є file_id. У разі автоматичної генерації його згодом можна отримати викликом tox_file_get_file_id, однак при передачі аватара його значення рекомендується встановлювати в результат виклику tox_hash від даних файлу аватара, що дозволяє приймаючій стороні скасовувати передачу раніше завантажених аватарів економлячи трафік.

Так само слід зазначити, що передача файлів можлива тільки тим друзям, які підключені до мережі. Відключення одного від мережі припиняє передачу файлу.

Після виклику tox_file_send ядро очікує рішення від приймаючої сторони. Рішення обробляється подією tox_file_recv_control_cb(friend_number, file_number, control), де:

  • friend_number — номер друга.
  • file_number — номер файлу.
  • control — команда управління файлом (TOX_FILE_CONTROL_CANCEL, TOX_FILE_CONTROL_PAUSE або TOX_FILE_CONTROL_RESUME розглянуті раніше).
Обробка цієї події дозволяє звільнити ресурси у разі відмови клієнта від прийняття файлу.

Echo-боту потрібно тільки передавати аватар. Передачу аватара рекомендується робити кожен раз, коли один з'являється в мережі. Якщо tox-клієнт одного раніше вже завантажив аватар з даними file_id, то він може скасувати повторну передачу аватара.

class EchoBot(ToxCore):
...
def __init__(self):
...
self.avatar_name = "avatar.png"
self.avatar_size = os.path.getsize(avatar_name)
self.avatar_fd = open(avatar_name, "rb")

data = self.avatar_fd.read()

self.avatar_id = ToxCore.tox_hash(data)

def tox_friend_connection_status_cb(self, friend_number, connection_status):
if connection_status != ToxCore.TOX_CONNECTION_NONE:
send_avatar(friend_number)

def send_avatar(self, friend_number):
file_number = self.tox_file_send(friend_number, ToxCore.TOX_FILE_KIND_AVATAR, self.avatar_size, self.avatar_id, self.avatar_name)

def tox_file_recv_control_cb(self, friend_number, file_number, control):
pass

def tox_file_chunk_request_cb(self, friend_number, file_number, position, length):
if length == 0:
return

self.avatar_fd.seek(position, 0)
data = self.avatar_fd.read(length)
self.tox_file_send_chunk(friend_number, file_number, position, data)


Крім прийому-передачі повідомлень і файлів, в ядрі реалізована можливість передачі пакетів (із втратами або без втрат). Для цього служать методи tox_friend_send_lossy_packet та tox_friend_send_lossless_packet, а так само події tox_friend_lossy_packet_cb іtox_friend_lossless_packet_cb.

Посилання
  • tox.chat — поточний офіційний сайт проекту Tox (замість tox.im).
  • Проект ядра toxcore на github.
  • Проект python-обгортки pytoxcore на github.
  • Проект для складання бінарних версій клієнтів і бібліотек tox.pkg на github.
  • Проект «офіційної» python-обгортки PyTox на github (на момент написання статті бібліотека не компилировалась через використання «старого» api).

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.