XMPP-бот на Java с использованием Smack API

    image

    Всем доброго времени суток!
    Тема написания ботов для жаббера довольно широко распространена. Но на хабре нашел всего одну статью, в которой бот был написан для сервера OpenFire. И в первом же комментарии написано, что было бы неплохо почитать про написание универсального бота, не привязанного к серверу. Так я и решил написать эту статью. Также расскажу про бота для Google Talk и один нюанс, связанный с этим ботом.


    Бот для jabber'а

    Как и в вышеуказанной статье, у меня есть OpenFire сервер, поэтому решил использовать их же библиотеку (это не значит, что бот будет работать только с моим сервером). Примеров для реализации бота довольно много, и вряд ли следующий код окажется сильно новым.

    public class Main
    {	
    	public static void main(String[] args)
    	{
    		try
    		{			
    			String botNick = "nickname";
    			String botPassword = "password";
    			String botDomain = "jabber.org";
    			String botServer = "jabber.org";
    			int botPort = 5222;
    			
    			JabberBot bot = new JabberBot(botNick, botPassword, botDomain, botServer, botPort);
    			Thread botThread = new Thread(bot);
    			botThread.start();
    		}
    		catch(Exception e)
    		{
    			System.out.printLn(e.getMessage());
    		}
    	}
    }
    
    /**
     * Бот, относящийся к одной учетной записи жабера.
     * Реализует интерфейс Runnable, так что разных ботов можно будет запускать
     * в разных потоках, или комбинировать их.<hr>
     *  
     *  Использует библиотеку smack.jar и smackx.jar:<br>
     *  org.jivesoftware.smack<hr>
     * 
     * @author esin
     *
     */
    public class JabberBot implements Runnable
    {
    	private String nick;
    	private String password;
    	private String domain;
    	private String server;
    	private int port;
    	
    	private ConnectionConfiguration connConfig;
    	private XMPPConnection connection;
    	
    	/**
    	 * В конструктор должны передаваться данные, необходимые для авторизации на жабер-сервере
    	 * @param nick - ник
    	 * @param password - пароль
    	 * @param domain - домен
    	 * @param server - сервер
    	 * @param port - порт
    	 */
    	public JabberBot (String nick, String password, String domain, String server, int port)
    	{
    		this.nick = nick;
    		this.password = password;
    		this.domain = domain;
    		this.server = server;
    		this.port = port;
    	}
    	
    	@Override
    	public void run()
    	{
    		connConfig = new ConnectionConfiguration(server, port, domain);
    		connection = new XMPPConnection(connConfig);
    		
    		try
    		{
    			int priority = 10;
    			SASLAuthentication.supportSASLMechanism("PLAIN", 0);
    			connection.connect();
    			connection.login(nick, password);
    			Presence presence = new Presence(Presence.Type.available);
    			presence.setStatus("статус бота");
    			connection.sendPacket(presence);
    			presence.setPriority(priority);
    						
    		    PacketFilter filter = new AndFilter(new PacketTypeFilter(Message.class));
    
    		    PacketListener myListener = new PacketListener() 
    		    {
    		        public void processPacket(Packet packet) 
    		        {
    		            if (packet instanceof Message) 
    		            {
    		                Message message = (Message) packet;
    		                // обработка входящего сообщения
    		                processMessage(message);
    		            }
    		        }
    		    };
    		    
    		    connection.addPacketListener(myListener, filter);
    		    
    		    // раз в минуту просыпаемся и проверяем, что соединение не разорвано
    		    while(connection.isConnected())
    		    {
    		    	Thread.sleep(60000);
    		    }
    		} 
    		catch (Exception e)
    		{
    			System.out.printLn(e.getMessage());
    		}
    	}
    	
    	/**
    	 * Обработка входящего сообщения<hr>
    	 * @param message входящее сообщение
    	 */
    	private void processMessage(Message message)
    	{
    		String messageBody = message.getBody();
    		String JID = message.getFrom();
    		
    		// обрабатываем сообщение. можно писать что угодно :)
    		// пока что пусть будет эхо-бот
    		sendMessage(JID, messageBody);
    	}
    	
    	/**
    	 * Отправка сообщения пользователю<hr>
    	 * @param to JID пользователя, которому надо отправить сообщение<br>
    	 * @param message сообщение
    	 */
    	private void sendMessage(String to, String message)
    	{
    		if(!message.equals(""))
    		{
    			ChatManager chatmanager = connection.getChatManager();
    			Chat newChat = chatmanager.createChat(to, null);
    			
    			try
    			{
    				newChat.sendMessage(message);
    			}
    			catch (XMPPException e)
    			{
    				System.out.printLn(e.getMessage());
    			}
    		}
    	}
    }
    


    Вот и все. Для чего я выделил бота в отдельный поток? Понимаю, что плодить потоки — не есть хорошо. Но этот поток после выполнения иниализации бота будет спать практически все время. Он только раз в минуту будет просыпаться для того, чтобы проверить соединение. Да и то это лишнее, но убрать нельзя было, т.к. иначе бот запустится и сразу выключится. Вся обработка входящих сообщений и отправка исходящий осуществляется в потоках, создаваемых библиотекой Smack.

    Бот для GoogleTalk

    Код этого бота практически ничем не отличается. Есть только пара отличий:

    public class Main
    {	
    	public static void main(String[] args)
    	{
    		try
    		{			
    			String botNick = "nickname";
    			String botPassword = "password";
    			String botDomain = "gmail.com"; // домен и сервер отличны друг от друга
    			String botServer = "talk.google.com";
    			int botPort = 5222;
    			
    			GoogleTalkBot bot = new GoogleTalkBot(botNick, botPassword, botDomain, botServer, botPort);
    			Thread botThread = new Thread(bot);
    			botThread.start();
    		}
    		catch(Exception e)
    		{
    			System.out.printLn(e.getMessage());
    		}
    	}
    }
    
    /**
     * Бот, относящийся к одной учетной записи гугл тока.
     * Реализует интерфейс Runnable, так что разных ботов можно будет запускать
     * в разных потоках, или комбинировать их.<hr>
     *  
     *  Использует библиотеку smack.jar и smackx.jar:<br>
     *  org.jivesoftware.smack<hr>
     * 
     * @author esin
     *
     */
    public class GoogleTalkBot implements Runnable
    {
    	private String nick;
    	private String password;
    	private String domain;
    	private String server;
    	private int port;
    	
    	private ConnectionConfiguration connConfig;
    	private XMPPConnection connection;
    	
    	/**
    	 * В конструктор должны передаваться данные, необходимые для авторизации в гугле
    	 * @param nick - ник
    	 * @param password - пароль
    	 * @param domain - домен
    	 * @param server - сервер
    	 * @param port - порт
    	 */
    	public GoogleTalkBot (String nick, String password, String domain, String server, int port)
    	{
    		this.nick = nick;
    		this.password = password;
    		this.domain = domain;
    		this.server = server;
    		this.port = port;
    	}
    	
    	@Override
    	public void run()
    	{
    		connConfig = new ConnectionConfiguration(server, port, domain);
    		connection = new XMPPConnection(connConfig);
    		
    		try
    		{
    			int priority = 10;
    			SASLAuthentication.supportSASLMechanism("PLAIN", 0);
    			connection.connect();
    			connection.login(nick + "@" + domain, password); // логин для авторизации нужно обязательно присылать в виде nickname@gmail.com
    			Presence presence = new Presence(Presence.Type.available);
    			presence.setStatus("статус бота");
    			connection.sendPacket(presence);
    			presence.setPriority(priority);
    						
    		    PacketFilter filter = new AndFilter(new PacketTypeFilter(Message.class));
    
    		    PacketListener myListener = new PacketListener() 
    		    {
    		        public void processPacket(Packet packet) 
    		        {
    		            if (packet instanceof Message) 
    		            {
    		                Message message = (Message) packet;
    		                if(message.getType() == Type.chat) // на всякий случай уточним, что нам нужны только сообщения чата
    		                {
    			                // Process message
    			                processMessage(message);
    		                }
    		            }
    		        }
    		    };
    		    
    		    // Register the listener.
    		    connection.addPacketListener(myListener, filter);
    		    
    		    while(connection.isConnected())
    		    {
    		    	Thread.sleep(60000);
    		    }
    		} 
    		catch (Exception e)
    		{
    			System.out.printLn(e.getMessage());
    		}
    	}
    	
    	/**
    	 * Обработка входящего сообщения<hr>
    	 * @param message - входящее сообщение
    	 */
    	private void processMessage(Message message)
    	{
    		String messageBody = message.getBody();
    		String JID = message.getFrom();
    		sendMessage(JID, messageBody);
    	}
    	
    	/**
    	 * Отправка сообщения пользователю<hr>
    	 * @param to - JID пользователя, которому надо отправить сообщение<br>
    	 * @param message - сообщение
    	 */
    	private void sendMessage(String to, String message)
    	{
    		if(!message.equals(""))
    		{
    			ChatManager chatmanager = connection.getChatManager();
    			Chat newChat = chatmanager.createChat(to, null);
    			
    			try
    			{
    				newChat.sendMessage(message);
    			}
    			catch (XMPPException e)
    			{
    				System.out.printLn(e.getMessage());
    			}
    		}
    	}
    }
    


    В общем, все почти также.
    Теперь о нюансе, который связан с ботом для гугл тока (в жабере этого нюанса нет). Я когда тестировал ботов, текст эхо-ответа от них посылал в виде
    <кто прислал>: <что прислал>
    т.е. в тексте явно указывал идентификатор (JID) того, кто присылал. Жабер работал отлично, без всяких сбоев. Гугл ток же после ровно 10 сообщений переставал присылать ответ. То, что это связано именно с логином в гугле, я выяснил после долгих мучений. Причем он не обязательно должен быть указан как nickname@gmail.com, достаточно будет просто nickname. Если логин не писать вообще или писать какой-нибудь другой, все работает отлично
    Поделиться публикацией

    Комментарии 11

      0
      использовал Smack API в одном из приложений для Android год назад… очень понятная библиотека, легко расширяемая и быстроподнимаемая. Кстати использовал как осному для XMPP мессенджера для vkontakte (гемора было больше с вконтактом чем с библиотекой).
        +1
        и один нюанс, связанный с этим ботом.

        Нюанс! Черт, это «сложное» слово так часто пишут через мягкий знак, что его правильное написание вызывает настоящую радость и уважение :)
          0
          О, благодарю! :)
          Я очень люблю, когда правильно пишут, и сам стараюсь по мере своих возможностей
          0
          Кстати, отправлять сообщение лучше (да и проще) тоже через connection.sendPacket, а не создавать чат для отправки одного мессажа ;)

          Хорошая библиотека, удобная и довольно гибкая. Но тонких и неуловимых глюков в ней просто куча :(
            0
            Спасибо! У меня была версия с отправкой сообщения через sendPacket, но бот страшно заглючил, и пришлось откатить. Пока оставил как есть.
            Не могли бы вы рассказать, что за глюки, или где о них можно почитать?
              +1
              С чатами были какие-то проблемы, по-моему они подвисали где-то внутри при регулярном create. А может я просто не умел их готовить и не делал какие-то инициализации или наоборот, освобождения. Документация на некоторый счёт весьма скудна. Через sendPacket всё работало точно, надо только пакет полностью заполнить (from/to итд).
              Про глюки, честно говоря, всего уже не вспомню. То коннект не получался, то память жрало в разных частях. Последнее что было (это уж не очень давно всплыло) — одна из крайних версий либы перестала соединяться при некоторых условиях (кажется, дело было в SASL авторизации), а так как использований наших продуктов в среднем довольно много и на разных платформах, и используются клиентами разные xmpp-сервера, то получалось что мы снова огребли от некоторого кол-ва наших клиентов. Пришлось гуглить, искать похожие баги (встречались многочисленные упоминания), рыть исходники итд итп. В итоге из релизной версии, исходников из svn-транка и некоторых рецептов из инета соорудил какие-то заплатки, кажется, до сих пор это и поставляется в сборках)
              Ну и постоянно какие-то мелочи были. Когда не выходишь за рамки принял/отправил, наверно и не заметно, а тут приходилось прикручивать к активной системе с огромной нагрузкой, и надо ещё следить чтобы не посыпалась память, чтобы потокобезопасно использовалось при этом и экономично. Ну, как-то так…
            0
            Огромное спасибо! Теперь буду знать чего и когда ждать
              0
              Тоже доволен этой библиотекой, бота пришлось написать потому что так и не смог найти где Jabber хранит информацию о времени подключения и отключения пользователей. А так, библиотека шикарная.
                0
                А у меня со smack проблема с отправкой сообщений в простейшей программе, если не ставить Thread.sleep(1000); сообщения иногда не отправляются, наверное метод sendMessage асинхронный и надо както отловить что он точно закончил отправку сообщения и только потом закрывать программу.
                chat.sendMessage(«Test!»);
                Thread.sleep(1000);
                connection.disconnect();
                Как бы избавиться от Thread.sleep(1000); чтобы количество сообщений было не 1 в секунду?
                  0
                  Даже не знаю, что вам подсказать. Уже год прошел почти, как я перестал работать с тем ботом.

                  Для начала попробуйте отловить исключение, вдруг что-нибудь покажет.

                  try
                  {
                      chat.sendMessage(message);
                  }
                  catch (XMPPException e)
                  {
                      System.out.printLn(e.getMessage());
                  }
                  

                  Есть еще одна версия: в более новой версии библиотеки smack изменился принцип отправки сообщений. Попробуйте почитать документацию.
                  Если не поможет — попробуйте воссоздать моего бота. Надеюсь, код подойдет для новой версии библиотеки. Не помню, чтобы у меня была такая проблема. Удачи!
                  0
                  >>Да и то это лишнее, но убрать нельзя было, т.к. иначе бот запустится и сразу выключится.

                  Object o = new Object();
                  synchronized(o){
                  o.wait();
                  }

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

                  Самое читаемое