Сношения с суперагентомИгорь Антонов (antonov.igor.khv@gmail.com) Хакер, номер #116, стр. 116-098-1 Кодим правильный Mail.AgentУ всеми любимой тети аси давно появился русский клон – «Mail.Агент». Не буду спорить, по популярности аська все еще впереди. Но уже сейчас понятно, что Mail.агента ждет большое будущее. А раз так, ты не должен терять времени зря – усаживайся поудобнее и приготовься постичь тонкости программирования мессенджера нового поколения. План действийКак ты понимаешь, Mail.Agent – это обычная сетевая программа, которая использует протокол поверх TCP/IP. Отсюда вывод: чтобы создать своего клиента, необходимо хорошенько раскурить этот протокол, выбрать способ реализации сетевой части и написать пару десятков строчек кода. По первому критерию все должно быть ясно, а вот на втором стоит остановиться. Ты уже, наверняка, в курсе, что на Delphi закодить любое приложение можно используя, минимум, две технологии: с помощью готовых классов и WinAPI-функций. Первый вариант зачастую проще, но зато второй – интересней. Вдобавок он позволяет понять принципы работы ОС. Для написания сетевых приложений в Windows есть целый набор сетевых функций – Winsock API. Про них я уже писал много раз и еще раз писать одно и то же просто не хочется. Поэтому мы рассмотрим готовый и, главное, универсальный компонент для работы с протоколом MMP, а исходник примера с реализацией на чистом WinSock API ты сможешь скачать со всем известного www.vr-online.ru. TMrim – быстрый путь в нирвануDelphi-программисты обычно на шаг впереди своих сишных коллег. Этот случай не исключение. Наш соотечественник Алексей Панов позаботился обо всех нас и закодил отличный компонент для комфортной работы с протоколом MMP. Свойства протокола MMP
События
ПрактикаТолку от неподкрепленной практикой теории мало. Поэтому запусти Delphi и приготовься кодить. Как обычно, сразу после запуска Delphi создаст новый пустой проект. Закрой его и загляни на наш DVD. Там тебя ждет компонент TMRim. Скопируй его на винт и заинсталь к своему Dlephi. Компонент устанавливается стандартным способом через Component -> Install Component. Установив компонент, создай новый проект и накидай на форму следующие компоненты:
По всей форме я растянул компонент TListView. В нем будет отображаться контакт-лист. Чтобы контакты не смотрелись серо и убого, я подобрал в TImageList картинки, соответствующие возможным статусам пользователей (Online, Away, Offline и т.д.). С помощью компонента TMainMenu я создал основное меню из следующих пунктов: подключить, отключить, выбор статуса (онлайн, отошел, невидимый). Готовый вид моей формы ты можешь увидеть на рисунке. На этом о форме можно забыть и приступить к кодингу. Первым делом научимся устанавливать и разрывать соединение с сервером. Для этого создай обработчик события OnClick для кнопки c именем «Подключить» и напиши в нем: If Form2.ShowModal = mrCancel Then Exit; ConnectBar.Visible := true; MailAgent.Login := Form2.LoginEdit.Text; MailAgent.Password := Form2.PassEdit.Text; MailAgent.LoginStatus := STATUS_ONLINE; MailAgent.login_s__desc := ‘Я в сети!!!'; MailAgent.Connect2(MailAgent.Login, MailAgent.Password); Перед тем, как начать попытку подключения, я модально показываю Form2 (смотри рисунок). На этой форме расположены два поля ввода, в которые нужно ввести имя пользователя и пароль. Если пароль и имя пользователя введены успешно, то можно начинать первую попытку соединения. Для этого я заполняю все необходимые свойства компонента TMrim (у меня он носит имя MailAgent). После того, как все поля заполнены, можно выполнить метод Connect2. Контекстно-зависимый скриншотПроцесс подключения запущен. Было бы неплохо иметь возможность отследить его текущее состояние на ProgressBar. Создай для компонента TMrim обработчик события OnConnectProgress и напиши в нем: ConnectBar.Position := State; If State = 100 Then begin Sleep(100); ConnectBar.Visible := false; end; Думаю, пояснять этот код нет смысла. Сложного в нем абсолютно ничего. Двигаемся дальше. С подключением разобрались, теперь нужно позаботиться о заполнении контакт-листа. Тебе уже известно, что получение списка контактов происходит при возникновении события OnList компонента TMrim, а раз так – создай обработчик события OnList и напиши в нем всего лишь две строчки кода: _ContactList := List; GetUserList (); В первой строчке кода я получаю в переменную _ContactList (объявлена как глобальная переменная типа AContact_List) ссылку на весь список контактов (List). После того, как ссылка получена, я обращаюсь к самописной процедуре GetUserList(), код которой ты можешь увидеть на соответствующей врезке. Что может быть проще, чем получить список контактов? procedure TForm1.GetUserList; var i:integer; _User : AUser; begin ListView1.Items.Clear; for i:=0 to _ContactList.users_num-1 do begin _User := _ContactList.users_id[i]; with ListView1.Items.Add do begin Caption := _User.user_nick; ImageIndex := GetStatusImage(_User.user_status, _user.user_status_id); end; end; end; Основная начинка процедуры GetUserList () – пробежка по структуре типа AContact_List, содержащей список контактов, и добавление всех найденных контактов в ListView. Обрати внимание, что в примере из структуры я извлекаю лишь ники пользователей. При программировании реального приложения этим можно не ограничиваться. Например, можно получить дату рождения пользователя, знак зодиака, e-mail и т.д. (полное описание структуры смотри во врезке). Добавляя очередной контакт в ListView, я определяю его статус и уже на основании его устанавливаю соответствующую картинку. Чтобы облегчить этот процесс, я написал функцию GetStatusImage(). В качестве параметров ей нужно передать статус и идентификатор статуса пользователя. Результатом будет число, соответствующее определенному индексу картинки в TImageList. Исходный код функции GetStatusImage() приведен во врезке. ЧатимсяПолучить список контактов в красивом виде – лишь полдела. Главная функция любого IM – предоставление возможности комфортного общения, для чего программисты обычно создают окно чата с закладками. Каждая такая закладка – это чат с определенным собеседником. Очень удобно и позволяет избавиться от проблемы «завала» всего рабочего стола «окнами». Я решил не отступать от этой идеи и реализовать в нашем примере подобный интерфейс. Сейчас я подробно расскажу, как это делается. Создай новую форму и брось на нее один компонент TPageControl (вкладки не создавай) и одну кнопку. У компонента TPageControl установи свойство Align в alTop (растянуть по верху). Теперь растяни этот компонент по всей оставшейся части формы, но не забудь оставить немного места под кнопку. По ее нажатию мы будем отправлять сообщения активному в данный момент собеседнику. Если ты запутался с расположением компонент, то не парься, а просто взгляни на рисунок и по нему подгони свою форму. Как закончишь с дизайном, возвращайся к модулю главной формы и объяви в ней новую процедуру: CreateNewTab (user_email:string; text:string), где user_email – e-mail пользователя, от которого пришло (или которому хотим отправить) сообщение. Этот мыльник у нас будет отображаться в качестве заголовка закладки, тем самым, идентифицируя собеседника. Смотрится не очень красиво (в реальном приложении лучше отображать ник пользователя). Весь код этой незатейливой процедуры ты можешь увидеть на соответствующей врезке. Быстренько начинай его переписывать, не забывая при этом иногда возвращаться к тексту статьи за разъяснениями. Изначально мы не можем знать точного количества собеседников, для которых в окне чата будем создавать закладки. Поэтому новые вкладки логичней всего создавать динамически – по мере необходимости. Скорее всего, после прочтения последнего предложения тебе стало смешно, – мол, как еще в такой ситуации можно поступить. Оказывается, можно. Однажды мне попался исходник довольно симпатичного асечного клиента. Когда я принялся его изучать и добрался до формы обмена сообщениями, – я ужаснулся. Автор программы создал почти сотню (!) вкладок и по мере необходимости игрался со свойством Visible. Но что-то я немного отвлекся. Поскольку все элементы формы сообщений мы будем создавать динамически, то на первом этапе нам нужно определить, какие потребуется создавать элементы управления. Обычно в подобных окнах размещают пару компонент типа TMemo (TRichEdit и всевозможные клоны). Один используется для отражения всей истории переписки, а второй – для ввода текста отправляемого сообщения. Помимо элементов ввода нам придется создавать новую закладку в компоненте PageControl. Итак, подведем итог. Для создания новой вкладки нам нужно создать три компонента: два TMemo и один TTabSheet. По этой причине в разделе var моей процедуры я объявил три соответствующие переменные: _NewTab типа TTabSheet (новая закладка для PageControl), _NewTextMemo типа TMemo (ввод текста для отправки) и _NewChatMemo (для отображения всей переписки) аналогичного типа. По моему замыслу, эта процедура должна вызываться каждый раз при получении нового сообщения. Следовательно, перед созданием новой вкладки нужно удостовериться, что для автора полученного сообщения еще не была создана закладка. Для проверки я запускаю цикл (for i:=0 to form3.PageControl1.PageCount-1), которой пробегает по всем существующим вкладкам и сравнивает их заголовок с e-mail отправителя. Если совпадение найдено, то нет смысла создавать еще одну закладку. Нужно просто добавить в компонент ChatMemoX полученный текст. Поскольку полного имени компонента (вместе с индексом) мы не знаем, то нам придется его найти с помощью метода FindComponent() элемента, на котором мы собственно, и ищем компонент. В нашем случае компонент ChatMemo будет располагаться на компоненте TPageControl, именно поэтому метод я вызываю следующим образом: Form3.PageControl.Pages[i].FindComponent(‘ChatMemo’+ IntToStr(i+1)). В качестве одного единственного параметра для метода FindComponent() я передаю имя искомого компонента, плюс его индекс, который будет равен индексу текущей закладки + 1 (так как первая вкладка имеет индекс 0). Компонент найден, – теперь надо добавить в него полученный текст. Чтобы это сделать, я привожу полученный в результате поиска объект к типу TMemo, а затем вызываю метод свойства Lines – Add(). Дальше все просто – добавив полученный текст, вызываю метод Show формы сообщений и выхожу из процедуры. Мы рассмотрели вариант с найденной вкладкой. А если она еще не была создана? Конечно же, нужно ее создать! Порядок очереди создания новых элементов будет таким:
Каждый визуальный компонент в Delphi – это обычный объект, который первым делом нужно инициализировать (выделить память). После можно заполнять все свойства, прикручивать обработчики событий и т.д. Единственный нюанс при создании визуальных компонент – необходимость «вставки» (указание родителя) этого компонента на любой другой. Иначе, как можно догадаться, – компонент не отобразится. Чтобы вставить вновь созданный компонент в форму, необходимо лишь вызвать у формы метод InsertControl(). В качестве единственного параметра нужно указать ссылку на проинициализированный элемент управления. function TForm1.GetStatusImage(status: integer; advStatusId:string): integer; begin Result := 1; Case Status Of STATUS_ONLINE: Result := 0; STATUS_OFFLINE: Result := 1; STATUS_AWAY: Result := 2; STATUS_FLAG_INVISIBLE : Result := 5; STATUS_USER_DEFINED: begin if advStatusId = 'status_chat' then Result := 3; if advStatusId = 'status_dnd' then Result := 4; end; end; end; procedure TForm1.CreateNewTab(user_email: string; text:string); var _NewTab : TTabSheet; _NewTextMemo, _NewChatMemo:TMemo; i:integer; begin for i:=0 to form3.PageControl1.PageCount-1 Do if form3.PageControl1.Pages[i].Caption = user_nick then begin form3.PageControl1.ActivePageIndex := i; if Text <> '' then TMemo(Form3.PageControl1.Pages[i]. FindComponent('ChatMemo'+IntToStr(i+1))). Lines.Add(user_nick+': '+text); form3.Show; Exit; end; _NewTab := TTabSheet.Create(form3.PageControl1); _NewTab.Caption := user_nick; _NewTab.Name := 'Tab'+IntToStr (form3.PageControl1.PageCount); _NewTab.PageControl := form3.PageControl1; _NewChatMemo := TMemo.Create(_NewTab); _NewChatMemo.Name := 'ChatMemo'+IntToStr( Form3.PageControl1.PageCount); _NewChatMemo.Align := alTop; _NewTab.InsertControl(_NewChatMemo); _NewTextMemo := TMemo.Create(_NewTab); _NewTextMemo.Name := 'TextMemo'+IntToStr( form3.PageControl1.PageCount); _NewTextMemo.Align := alTop; _NewTab.InsertControl(_NewTextMemo); _NewChatMemo.Lines.Clear; _NewTextMemo.Lines.Clear; If Text <> '' Then _NewChatMemo.Lines.Add(user_nick+': '+text); Form3.PageControl1.ActivePage := _NewTab; Form3.Show; end; AContact_List AContact_List = record //Количество групп group_num: integer; //Массив структур типа AGroup groups_id: array[0..20] of AGroup; //Массив структур типа AUser users_id: array[0..1023] of AUser; //Общие количество контактов users_num: integer; AUser = record user_flags: integer; user_gnum: integer; //Email пользователя user_mail: string; //Nick пользователя user_nick: string; //Имя пользователя user_name: string; //Фамилия пользователя user_lastname: string; //Дата рождения user_bday: string; //Место жительства user_location: string; //Флаги server_flags: integer; //Статус пользователя user_status: integer; //Телефон user_phone: string; //Идентификатор пользователя user_id: integer; //Идентификатор статуса user_status_id, //статус пользователя user_status_name, //Оисание статуса user_status_desc: string; //Идентификатор клиента пользоваля user_client_id: string; end; Тестируем клиентаКлиент готов, теперь самое время его хорошенько протестировать. Я специально завел пару аккаунтов на mail.ru: один для только что испеченного нами клиента (user_test_2008@mail.ru), а второй – для Mail.Agentа (user_test_2009@mail.ru). Оба клиента успешно соединились с сервером и обменялись несколькими сообщениями. Результат ты можешь увидеть на рисунках (с соответствующими подписями). Happy EndЛекцию рассмотрения внутренностей протокола MMP можно считать оконченной. Тебе остается переварить знания и воспользоваться ими в своих хацкерских целях. Полученной инфы вполне хватит для написания полноценного Mail.Agent’a или спамбота. К сожалению, спецификация протокола уже давно не обновлялась, а значит, шагать в ногу с Mail.Agent’ом, увы, не получится. Будем надеяться, что девелоперы MA расщедрятся и полностью откроют протокол со всеми вкусностями (передача видеоданных и т.д.). Пока нам остается лишь пользоваться тем, что есть. На этой немного грустной ноте я хочу попрощаться и пожелать тебе больше позитива и поменьше Access Violition. Пока! P.S. Автор выражает огромную благодарность Алексею Панову за идею статьи и отличный компонент для работы с MMP. WWWДополнительный исходник (на чистом WinSock API) ты можешь скачать с сайта www.vr-online.ru после выхода журнала в свет. CDКак обычно, исходник примера, а также компонент можно взять на нашем диске. |