пʼятницю, 27 листопада 2015 р.

Мессенджер Telegram

Краткое вступление

Мой первый опыт общения с помощью Всемирной сети Интернет - электронная почта. Конечно же, почта будет жить всегда, но потом появились другие потребности - к примеру общение голосом, возможность увидеть есть ли онлайн тот или иной контакт, удобство общения (особенно для мобильных телефонов).
Так позже Я познакомился с ICQ (I seek you, аська). Этот сервис уже существовал относительно долго, мне удалось даже попользоваться мобильным клиентом QIP, который работал на многих телефонах (до эры смартфонов), которые позволяли устанавливать скачанные джава приложения (очень популярная была тогда линейка телефонов Sony Ericsson). В то время (примерно 2005) тарифы на мобильный Интернет были довольно высоки, да и мобильная связь тоже не была очень дешевой. Мобильный QIP потреблял очень мало трафика и позволял связываться с людьми по всему миру, в чём определённо была выгода.
Прошло ещё немножко времени - и компьютер уже почти в каждом доме, смартфон уже почти у каждого человека. Skype быстро набирал популярность, в то время как аська теряла. Конечно, множество людей также регистрировалось в разных социальных сетях и предпочитало общение там же. Но со skype можно было делать даже видеозвонки почти в любую точку мира, а Интернет уже был быстрым и дешевым. Но вот случилася беда - Microsoft выкупила Skype. Правда, он и чуть раньше начал портиться - старые смартфоны больше его не поддерживали, а новым, но с маленькими ресурсами - skype был уже не под силу. Кроме того, некоторые люди очень беспокоились тем, что skype "прослушивается" (многие также говорят, что им скрывать нечего, но довольно неприятно осознавать сам факт прослушки). Странички в соцсетях и без всяких специальных средств как на ладони, а если ещё и "попросят", то можно расковырять всё о владельце аккаунта. И вот немалоизвестный создатель одной из таких соцсетей - Павел Дуров (кстати, довольно интересная история его выезда из родной страны, обязательно почитайте) создаёт новый месенджер - Telegram.

Telegram

Вышел этот мессенджер в 2013 году, казалось бы чем он мог соревноваться с уже существующими мессенджерами. Но, пройдя на страничку Telegram, можно увидеть, что он предлагает следующие преимущества:
- бесплатный навсегда, без реклам, без взносов;
- супер защищенный; говорят, что не дадут доступа к перепискам даже на запросы разных спецслужб;
- легковесный, работает на любой популярной платформе, а также просто в браузере;
- клиент с открытым кодом (сервер - нет).
Ну и поскольку он привязан к телефонному номеру, то автоматически импортирует конакты всех людей из вашей телефонной книги, которые уже зарегистрированы в Telegram.
А в 2015 году появились ещё и программируемые боты для Telegram. Суть в том, что Вы можете отсылать вашему боту команду как обычное сообщение (каждый созданный вами бот будет висеть в списке контактов), а ваша программа может парсить эту команду, выполнить её и прислать вам ответ в тот же чат с ботом (или прислать файл, или ничего не присылать, а просто что-то выполнить). Это очень удобно, если Вам, к примеру, надо следить за "здоровьем" какого-то вашего сервера с любой точки планеты.

Telegram Bot

Рассмотрим как это работает на простом примере. Будем писать бота, который читает и копирует файлы в директорию Dropbox. Код, который Вы здесь увидите, не идеален, но может послужить Вам стартовой площадкой.
Все запросы к API телеграм бота делаются через HTTPS протокол и имеют такой формат:
https://api.telegram.org/bot<token>/<method>

Например: 

https://api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getMe

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

Ответ на любой запрос приходит в виде красивого JSON. Если отправить боту сообщение "Hello World", а потом проверить получил ли бот что-то новенькое, то это всё будет выглядеть примерно так:

 $ curl https://api.telegram.org/bot$TOKEN/getUpdates
 

{"ok":true,"result":[{"update_id":373347763,
"message":{"message_id":121,"from":{"id":119435478,"first_name":"Sergius","last_name":"Master"},"chat":{"id":119435478,"first_name":"Sergius","last_name":"Master","type":"private"},"date":1447629425,"text":"Hello World"}}]}

Итак, наша задача - распарсить этот JSON и послать соответствующий ему ответ (или не послать, если нет соответствия).
Для отправки ответа нужно использовать метод "sendMessage", где обязательные парамеры это сам текст сообщения и идентификатор чата - куда послать (с консоли надо "ескейпить" символ "&"):
$ curl https://api.telegram.org/bot$TOKEN/sendMessage?text=hello+from+bot\&chat_id=119435478

Для написания бота Я использовал мой любимый ЯП - Python. Естественно, принимать и отправлять сообщения будем не через curl, а через высокоуровневые библиотеки Python'a (urllib, urllib2).
 
#!/usr/bin/env python
import json
import logging
import logging.handlers
import os
import re
import shutil
import signal
import urllib
import urllib2

from contextlib import closing
from glob import glob
from string import Template
from time import sleep

HTTP_API_TEMPLATE = Template("https://api.telegram.org/bot${TOKEN}/${METHOD}")
API_TOKEN = "!!!!! HERE MUST BE YOUR API TOKEN !!!!!"

class TelegramBot(object):

    should_continue = False

    def __init__(self):
        self.last_update_id = None

        self._log = logging.getLogger('file_reader')
        self._log.setLevel(logging.INFO)
        handler = logging.handlers.SysLogHandler(address = '/dev/log')
        log_format = "%(filename)s: %(levelname)s: %(message)s"
        fmt = logging.Formatter(log_format)
        handler.setFormatter(fmt)
        self._log.addHandler(handler)

    def get_last_message(self):
        if self.last_update_id:
            response = self.get_updates("?offset=%s" % (self.last_update_id + 1))
        else:
            response = self.get_updates()

        resp_dict = json.loads(response)
        if resp_dict.get("result", []):
            last_msg_dict = resp_dict["result"][-1]
            self.last_update_id = int(last_msg_dict.get("update_id", 0))
            return { "from": last_msg_dict["message"]["chat"]["id"],
                     "cmd": last_msg_dict["message"]["text"] }

    def get_updates(self, options=""):
        subs_dict = {'TOKEN': API_TOKEN, 'METHOD': 'getUpdates%s' % options}
        get_message_url = HTTP_API_TEMPLATE.substitute(subs_dict)
        response = ""
        with closing(urllib2.urlopen(get_message_url)) as get_upd:
            response = get_upd.read()
        return response

    def send_response(self, text, send_to):
        options = "?" + urllib.urlencode({'chat_id': send_to, 'text': text})
        subs_dict = {'TOKEN': API_TOKEN, 'METHOD': 'sendMessage%s' % options}
        send_message_url = HTTP_API_TEMPLATE.substitute(subs_dict)
        response = ""
        print send_message_url
        with closing(urllib2.urlopen(send_message_url)) as send_msg:
            response = send_msg.read()

    def parse_command(self, command):
        command = command.split()
        if len(command) == 1:
            cmd, args = command[0], ""
        elif len(command) == 2:
            cmd, args = command
        else:
            return None, None
        match = re.search("/\w+", cmd.strip())
        if match:
            cmd = match.group()
            if re.match("^\S*$", args.strip()):
                return cmd, args.strip()
        return None, None

    def execute_command(self, command):
        """ Available commands:

            /help - shows this documentation
            /ls <directory>* - shows directory listing
                type /ls <directory>*.txt to show all txt files in directory
            /cat <file> - shows file
            /head <file> - shows first 10 lines of file
            /tail <file> - shows last 10 lines of file
            /cpd <file> - copy file to Dropbox/bot
        """
        cmd, args = self.parse_command(command)
        if cmd == "/help":
            return self.execute_command.__doc__
        if cmd == "/ls":
            return "\n".join(glob(args))
        if cmd == "/cat":
            try:
                output = ""
                with open(args) as f:
                    output = f.read()
                return output
            except IOError as e:
                return str(e)
        if cmd == "/head":
            try:
                output = ""
                with open(args) as f:
                   output = "".join(f.readlines()[:10])
                return output
            except IOError as e:
                return str(e)
        if cmd == "/tail":
            try:
                output = ""
                with open(args) as f:
                   output = "".join(f.readlines()[-10:])
                return output
            except IOError as e:
                return str(e)
        if cmd == "/cpd":
            DropboxFolder = "/home/sergius/Dropbox/bot"
            destination = "%s/%s" % (DropboxFolder, os.path.basename(args))
            try:
                shutil.copyfile(args, destination)
                return "Copied file successfully"
            except Exception as e:
                return str(e)

    def check_updates_for_command(self):
        msg = None
        try:
            msg = self.get_last_message()
        except Exception as e:
            self._log.error("Network Error: %s " % str(e))
            sleep(10)
            return None
        if msg:
            # Easter Egg ---
            if msg["cmd"].lower() == "good job":
                response = "Thank you, Master"
            # ---
            else:
                response = self.execute_command(msg["cmd"])
            if response:
                self.send_response(response, msg["from"])

    def doit(self):
        self._log.info('Starting telegram bot "file_reader"')
        TelegramBot.should_continue = True
        while TelegramBot.should_continue:
            self.check_updates_for_command()
            sleep(1)
        self._log.info('Stopping telegram bot "file_reader"')

    @staticmethod
    def signal_handler(signum, frame):
        TelegramBot.should_continue = False

signal.signal(signal.SIGTERM, TelegramBot.signal_handler)

def main():
    bot = TelegramBot()
    bot.doit()

if __name__ == "__main__":
    main()


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