python

Подробнее о протоколе Mail.Ru Агент

  • среда, 18 марта 2015 г. в 02:10:53
http://habrahabr.ru/post/253303/

На Хабре уже писали о том, как устроен Mail.Ru Агент. На данный момент официальной документации к протоколу в открытом доступе нет, поэтому приходится исследовать устройство опытным путем. В этой статье я рассмотрю отправление форматированных текстовых сообщений и создание и отправление сообщений в конференцию.

Пара слов о протоколе


Сообщения передаются пакетами определенного формата. Первые 44 байта — это заголовок, который выглядит так:

struct.pack(
                '<7L16s',          
                magic,      # DEAD BEEF, ключевые слова, которые показывают, что это именно MMP протокол 
                proto,      # версия протокола
                seq,        # порядковый номер сообщения
                msg,        # тип пакета: сообщение/ запрос на авторизацию/...
                dlen,       # длина пакета 
                from_,      # адрес отправителя, для исходящего сообщения значение нулевое
                fromport,   # порт отправителя, для исходящего сообщения тоже нулевое значение
                reserved    # зарезервированные 16 байт
                )


Числа здесь передаются в формате UL, который выглядит как 16 байт, записанных справа налево. Таким образом, число 10 будет выглядеть 00 00 00 0A. Так мы будем запаковывать в UL:


class MRIMType(object):
    def __init__(self, value):
        self.value = value

class UL(MRIMType):
    def pack(self):
        if isinstance(self.value, int):
            return struct.pack("<L", self.value)
        else:
            return struct.pack("<L", len(self.value))


Текст передается в формате LPS — строки с заданной длиной (длина задается в виде UL). Мы будем запаковывать в него таким образом:

class LPSZ(MRIMType):
    def pack(self):
        if isinstance(self.value, (list, tuple)):  # пакуем списки и тьюплы поэлементно
            data = ""
            data += UL(self.value).pack()
            for element in self.value:
                data += element.pack()
            return data
        elif isinstance(self.value, MRIMType):  # пакуем уже запакованные элементы
            data = self.value.pack()

            return struct.pack("<L", len(data)) + data
        else:
            data = self.value
            return struct.pack("<L", len(data)) + data


Также нам понадобится упаковывать строки в LPS в других кодировках:

class LPSX(MRIMType):
    def __init__(self, value, encoding):
        super(LPSX, self).__init__(value)
        self.encoding = encoding

    def pack(self):
        encoded_data = self.value.encode(self.encoding)
        return struct.pack("<L", len(encoded_data)) + encoded_data


class LPSW(LPSX):
    def __init__(self, value):
        super(LPSW, self).__init__(value, encoding="UTF-16LE")

class LPSA(LPSX):
    def __init__(self, value):
        super(LPSA, self).__init__(value, encoding="cp1251")
 

Текстовые сообщения с форматированием


Посмотрим, как выглядят сообщения. Поле msg в заголовке должно быть заполнено константой 0x1008, в остальном пакет сообщения такой:

 HEADER
 UL(0x80),
 LPSA(recipient),  # email получателя сообщения
 LPSW(body), # собственно, текст сообщения
 LPSZ(rtf_part) 

Последняя составляющая пакета — часть сообщения, связанная с форматированием текста. Если нам не нужно форматирование, rtf_part должна состоять из пробела. В таком случае Mail.Ru Агент, на который придет это сообщение, будет использовать шрифты, установленные по умолчанию в агенте получателя.

Если мы хотим послать отформатированное сообщение, то последняя часть пакета должна быть LPSZ(rtf_part), где:

rtf_part = pack_rtf(rtf)

def pack_rtf(rtf):
    msg = UL(2).pack() + LPSZ(rtf).pack() + LPSZ("\xFF\xFF\xFF\x00").pack()
    message = base64.b64encode(msg.encode('zlib'))
    return message


Последнее слагаемое — цвет фона, при получении сообщения окно чата сменит цвет целиком.
rtf для написания «qwerty» выглядит так:

r"{{\rtf1\ansi\ansicpg1252\deff0\deflang1033" \ #поддерживаем русский
r"{{\fonttbl{{\f0\fnil\fcharset204 Tahoma;}}{{\f1\fnil\fcharset0 Tahoma;}}}}\  #  шрифты, которыми будем писать. обычно их два
r"{{\colortbl ;\red0\green0\blue0;}} " \  # цвет шрифта
r"\viewkind4\uc1\pard\cf1\f0\fs32 q\f1 werty\f0\par }}"  # собственно, текст

Можно заметить, что первая буква написана одним шрифтом, а остальные другим. Объяснить такое поведение я не могу, но rtf, сгенерированные Mail.Ru Агент, которые мне удалось получить, выглядели так. rtf, не обладающие таким свойством, остаются валидными. Остальные параметры (язык, таблица шрифтов, русский язык) влияют на валидность rtf.

Остается отметить, что если rtf-часть сообщения не пуста, она придет в сообщении. Если при этом указана текстовая часть сообщения (body), то этот текст мы увидим во всплывающем окне Mail.Ru Агент.

Конференции


Если для того, чтобы начать чат с другим контактом, нужно просто отправить сообщение, то для того, чтобы начать общаться в конференции, нужно сделать несколько приседаний.

Создание конференции


Каждая конференция имеет свое уникальное имя, которое выглядит как 5895986@chat.agent, которое мы получаем от сервера в ответ на такое сообщение:

HEADER # сообщение с кодом 0x1019
UL(0x80), 
LPSW(""),
LPSW(""),
LPSW(chat_name), # название, с которым мы хотим создать чат
LPSW(""),
LPSW(""),
LPSW(""),
LPSZ(LPSZ(LPSZ(packed_recipients))) # трижды запакованный список участников

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

Отправление сообщений в конференцию


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

HEADER # тип пакета -- сообщение (0x1008)
UL(0x200400),
LPSA(chat_alias), #айдишник чата, который мы получили от сервера
LPSZ(" "),
LPSZ("")

А теперь, непосредственно, сообщение:

HEADER
UL(0x80), 
LPSA(chat_alias),  
LPSW(body),
LPSZ(rtf_part) 

Оно выглядит как обычное сообщение с получателем-айдишником конференции.

Выход из конференции


Данные исследования были нужны мне для реализации автотестов. Я достаточно быстро столкнулась с проблемой — аккаунт может находиться в конечном количестве конференций. Поэтому по окончанию теста нужно покидать конференцию.

HEADER # тип сообщения, соответствующий изменению контакта 0x101B
UL(absolute_chat_number), # номер конференции, считая с начала времен. 
UL(0x0081), 
UL(0xF4244),
LPSZ(chat_alias), # айдишник конференции
LPSW(chat_name), # название конференции
LPSZ(""),

Не удалось выяснить, как получить абсолютный номер конференции, но экспериментально выяснено, что идентификация чата происходит не по нему. Поэтому можно указать любое разумное число, например, 42.

Мое исследование далеко от того, чтобы быть полным, поэтому буду рада любым исправлениям и дополнениям.