Программируем клиент для Mail.Agent
Рубрика: Статьи -> Журнал Хакер -> Программирование -> Delphi
Метки: delphi | sockets
Просмотров: 7933
У всеми любимой тети аси давно появился русский клон – «Mail.Агент». Не буду спорить, по популярности аська все еще впереди. Но уже сейчас понятно, что Mail.агента ждет большое будущее. А раз так, ты не должен терять времени зря – усаживайся поудобнее и приготовься постичь тонкости программирования мессенджера нового поколения.
План действий
Как ты понимаешь, Mail.Agent – это обычная сетевая программа, которая использует протокол поверх TCP/IP. Отсюда вывод: чтобы создать своего клиента, необходимо хорошенько раскурить этот протокол, выбрать способ реализации сетевой части и написать пару десятков строчек кода. По первому критерию все должно быть ясно, а вот на втором стоит остановиться. Ты уже, наверняка, в курсе, что на Delphi закодить любое приложение можно используя, минимум, две технологии: с помощью готовых классов и WinAPI-функций. Первый вариант зачастую проще, но зато второй – интересней. Вдобавок он позволяет понять принципы работы ОС. Для написания сетевых приложений в Windows есть целый набор сетевых функций – Winsock API. Про них я уже писал много раз и еще раз писать одно и то же просто не хочется. Поэтому мы рассмотрим готовый и, главное, универсальный компонент для работы с протоколом MMP, а исходник примера с реализацией на чистом WinSock API ты сможешь скачать со всем известного www.vr-online.ru.
TMrim – быстрый путь в нирвану
Delphi-программисты обычно на шаг впереди своих сишных коллег. Этот случай не исключение. Наш соотечественник Алексей Панов позаботился обо всех нас и закодил компонент для комфортной работы с протоколом MMP.
Свойства протокола MMP
- STATUS_ONLINE – онлайн
- STATUS_AWAY – отошел (нет на месте)
- STATUS_FLAG_INVISIBLE – флаг невидимости. Этот флаг нужно использовать, как правило, со STATUS_ONLINE
События
Практика
Толку от неподкрепленной практикой теории мало. Поэтому запусти 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. Пока!
Статья опубликована в журнале "Хакер" (http://xakep.ru). Август 2008 г.
Ссылка на журнал: http://goo.gl/YTLMNp
P.S. Друзья, перед тем как писать комментарии в стиле "Автор, на дворе 2014 год, на фиг нам Mail.Agent на Delphi", обрати внимание на дату статьи - август 2008. На блоге я собираю все свои статьи, которые писал для журналов. Увы, некоторые из них утратили актуальность.