Openfire插件开发心得:基于XMPP实现类Twitter
Openfire是XMPP协议最好的服务器软件。最近修改了一个插件实现了类Twitter功能,发出来分享
Get started
我用的是debian作为Openfire服务器
下载Openfire,安装JRE(Jaav运行库),把Openfire跑起来
下载JDK 1.5,安装到/opt/jdk1.5.0_14
下载openfire源码,准备编译。当时我安装的oenfire是3.4.1所以源码也下载的是3.4.1版本。用tar -zxvf openfire_src_3_4_1.tar.gz
解压到/opt/openfire_src
配置系统环境,声明JDK的环境变量export JAVA_HOME=/opt/jdk1.5.0_14/
下载ant编译工具,apt-get install ant
呵呵,debian就是爽。
编写Openfire插件
这是一个及其痛苦的过程,由于我不会Java,所以报了本《Thinking in Java》然后对照着SUN的官方文档边学边写,最后还是鼓捣出来了,呵呵
我的插件是基于Stefan Reuter的userstatus插件修改的
User Status Plugin is a plugin for the Openfire XMPP server to save the user status to the database.This plugin automatically saves the last status (presence, IP address, logon and logoff time) per user and resource to userStatus table in the Openfire database.
Optionally you can archive user status entries (IP address, logon and logoff time) for a specified time. History entries are stored in the userStatusHistory table. The settings for history archiving can be configured on the "User Status Settings" page that you'll find on the "Server" tab of the Openfire Admin Console.
目录结构有些变化,我的修改版可以在这里下载 userstatus.rar
目录结构是:
E:\\DORMFORCE\\NUTALK\\OPENFIRE\\EST\\USER-STATUS
│ changelog.html
│ plugin.xml
│ readme.html
│
├─i18n
│ user-status_i18n.properties
│
├─META-INF
│ │ MANIFEST.MF
│ │
│ └─maven
│ └─com.reucon.openfire.plugins
│ └─user-status
│ pom.properties
│ pom.xml
│
├─src
│ ├─database
│ │ user-status_mysql.sql
│ │
│ ├─java
│ │ └─com
│ │ └─reucon
│ │ └─openfire
│ │ └─plugins
│ │ └─userstatus
│ │ UserStatusPlugin.java
│ │
│ └─web
│ user-status-settings.jsp
│
└─web
│ user-status-settings.jsp
│
└─WEB-INF
web.xml
修改了user-status_mysql.sql
,在userStatus
表增加一个字段status TEXT,
,然后修改UserStatusPlugin.java
的源码了
[code:java]
package com.reucon.openfire.plugins.userstatus;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.event.SessionEventDispatcher;
import org.jivesoftware.openfire.event.SessionEventListener;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.user.PresenceEventDispatcher;
import org.jivesoftware.openfire.user.PresenceEventListener;
import org.jivesoftware.util.*;
import org.xmpp.packet.Presence;
import org.xmpp.packet.JID;
import java.io.File;
import java.net.UnknownHostException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
import java.util.Map;
//by est
import java.net.URL;
import java.net.URLEncoder;
import java.net.HttpURLConnection;
/**
* UserStatus plugin for Openfire. MOD by est
*/
public class UserStatusPlugin implements Plugin, PropertyEventListener, SessionEventListener, PresenceEventListener
{
private static final int SEQ_ID = 510;
private static final String ADD_USER_STATUS =
"Insert INTO userStatus (username, resource, online, lastIpAddress, lastLoginDate) " +
"VALUES (?, ?, 1, ?, ?)";
private static final String UPDATE_USER_STATUS =
"UPDATE userStatus SET online = 1, lastIpAddress = ?, lastLoginDate = ? " +
"Where username = ? AND resource = ?";
private static final String SET_PRESENCE =
"UPDATE userStatus SET presence = ?, status = ? Where username = ? AND resource = ?"; //By est
private static final String SET_OFFLINE =
"UPDATE userStatus SET online = 0, lastLogoffDate = ? Where username = ? AND resource = ?";
private static final String SET_ALL_OFFLINE =
"UPDATE userStatus SET online = 0";
private static final String ADD_USER_STATUS_HISTORY =
"Insert INTO userStatusHistory (historyID, username, resource, lastIpAddress," +
"lastLoginDate, lastLogoffDate) VALUES (?, ?, ?, ?, ?, ?)";
private static final String DELETE_OLD_USER_STATUS_HISTORY =
"DELETE From userStatusHistory Where lastLogoffDate < ?"; public static final String HISTORY_DAYS_PROPERTY = "user-status.historyDays"; public static final int DEFAULT_HISTORY_DAYS = -1; /** * Number of days to keep history entries.
* 0 for now history entries, -1 for unlimited.
*/
private int historyDays = DEFAULT_HISTORY_DAYS;
public static final String strSynXCallHubUrlFormat = "http://www2.dormforce.net/services/callhub.php?SynXPwd=___&username=%s&status=%s";
public void initializePlugin(PluginManager manager, File pluginDirectory)
{
Connection con = null;
PreparedStatement pstmt = null;
historyDays = JiveGlobals.getIntProperty(HISTORY_DAYS_PROPERTY, DEFAULT_HISTORY_DAYS);
PropertyEventDispatcher.addListener(this);
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(SET_ALL_OFFLINE);
pstmt.executeUpdate();
}
catch (SQLException e)
{
Log.error("Unable to clean up user status", e);
}
finally
{
DbConnectionManager.closeConnection(pstmt, con);
}
for (ClientSession session : SessionManager.getInstance().getSessions())
{
sessionCreated(session);
}
SessionEventDispatcher.addListener(this);
PresenceEventDispatcher.addListener(this);
}
public void destroyPlugin()
{
PresenceEventDispatcher.removeListener(this);
SessionEventDispatcher.removeListener(this);
}
public void sessionCreated(Session session)
{
Connection con = null;
PreparedStatement pstmt = null;
int rowsUpdated = 0;
if (!XMPPServer.getInstance().getUserManager().isRegisteredUser(session.getAddress()))
{
return;
}
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_USER_STATUS);
pstmt.setString(1, getHostAddress(session));
pstmt.setString(2, StringUtils.dateToMillis(session.getCreationDate()));
pstmt.setString(3, session.getAddress().getNode());
pstmt.setString(4, session.getAddress().getResource());
rowsUpdated = pstmt.executeUpdate();
}
catch (SQLException e)
{
Log.error("Unable to update user status for " + session.getAddress(), e);
}
finally
{
DbConnectionManager.closeConnection(pstmt, con);
}
if (rowsUpdated == 0)
{
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_USER_STATUS);
pstmt.setString(1, session.getAddress().getNode());
pstmt.setString(2, session.getAddress().getResource());
pstmt.setString(3, getHostAddress(session));
pstmt.setString(4, StringUtils.dateToMillis(session.getCreationDate()));
pstmt.executeUpdate();
}
catch (SQLException e)
{
Log.error("Unable to Insert user status for " + session.getAddress(), e);
}
finally
{
DbConnectionManager.closeConnection(pstmt, con);
}
}
}
public void sessionDestroyed(Session session)
{
Connection con = null;
PreparedStatement pstmt = null;
final Date logoffDate;
if (!XMPPServer.getInstance().getUserManager().isRegisteredUser(session.getAddress()))
{
return;
}
logoffDate = new Date();
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(SET_OFFLINE);
pstmt.setString(1, StringUtils.dateToMillis(logoffDate));
pstmt.setString(2, session.getAddress().getNode());
pstmt.setString(3, session.getAddress().getResource());
pstmt.executeUpdate();
}
catch (SQLException e)
{
Log.error("Unable to update user status for " + session.getAddress(), e);
}
finally
{
DbConnectionManager.closeConnection(pstmt, con);
}
// write history entry
if (historyDays != 0)
{
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(ADD_USER_STATUS_HISTORY);
pstmt.setLong(1, SequenceManager.nextID(SEQ_ID));
pstmt.setString(2, session.getAddress().getNode());
pstmt.setString(3, session.getAddress().getResource());
pstmt.setString(4, getHostAddress(session));
pstmt.setString(5, StringUtils.dateToMillis(session.getCreationDate()));
pstmt.setString(6, StringUtils.dateToMillis(logoffDate));
pstmt.executeUpdate();
}
catch (SQLException e)
{
Log.error("Unable to add user status history for " + session.getAddress(), e);
}
finally
{
DbConnectionManager.closeConnection(pstmt, con);
}
}
deleteOldHistoryEntries();
}
public void anonymousSessionCreated(Session session)
{
// we are not interested in anonymous sessions
}
public void anonymousSessionDestroyed(Session session)
{
// we are not interested in anonymous sessions
}
public void availableSession(ClientSession session, Presence presence)
{
updatePresence(session, presence);
}
public void unavailableSession(ClientSession session, Presence presence)
{
updatePresence(session, presence);
}
public void presencePriorityChanged(ClientSession session, Presence presence)
{
updatePresence(session, presence);
}
public void presenceChanged(ClientSession session, Presence presence)
{
updatePresence(session, presence);
}
public void subscribedToPresence(JID subscriberJID, JID authorizerJID)
{
// we are not interested in subscription updates
}
public void unsubscribedToPresence(JID unsubscriberJID, JID recipientJID)
{
// we are not interested in subscription updates
}
public void propertySet(String property, Map params)
{
if (HISTORY_DAYS_PROPERTY.equals(property))
{
final Object value = params.get("value");
if (value != null)
{
try
{
historyDays = Integer.valueOf(value.toString());
}
catch (NumberFormatException e)
{
historyDays = DEFAULT_HISTORY_DAYS;
}
deleteOldHistoryEntries();
}
}
}
public void propertyDeleted(String property, Map params)
{
if (HISTORY_DAYS_PROPERTY.equals(property))
{
historyDays = DEFAULT_HISTORY_DAYS;
deleteOldHistoryEntries();
}
}
public void xmlPropertySet(String property, Map params)
{
// we don't use xml properties
}
public void xmlPropertyDeleted(String property, Map params)
{
// we don't use xml properties
}
private void updatePresence(ClientSession session, Presence presence)
{
Connection con = null;
PreparedStatement pstmt = null;
String presenceText="";
String statusText="";
if (!XMPPServer.getInstance().getUserManager().isRegisteredUser(session.getAddress()))
{
return;
}
if (Presence.Type.unavailable.equals(presence.getType()))
{
presenceText = presence.getType().toString();
}
else if (presence.getShow() != null)
{
presenceText = presence.getShow().toString();
}
else if (presence.isAvailable())
{
presenceText = "available";
}
else
{
return;
}
try
{
statusText = presence.getStatus();
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(SET_PRESENCE);
pstmt.setString(1, presenceText);
pstmt.setString(2, statusText); //By est, following numbers are added by 1.
pstmt.setString(3, session.getAddress().getNode());
pstmt.setString(4, session.getAddress().getResource());
pstmt.executeUpdate();
/*SynX CallHub by est*/
URL url = new URL(String.format(strSynXCallHubUrlFormat,URLEncoder.encode( session.getAddress().getNode() ), URLEncoder.encode( statusText, "UTF-8" )));
HttpURLConnection httpUrl = (HttpURLConnection)url.openConnection();
httpUrl.connect();
}
catch (Exception e)
{
Log.error("Unable to update presence for " + session.getAddress(), e);
}
finally
{
DbConnectionManager.closeConnection(pstmt, con);
}
}
private void deleteOldHistoryEntries()
{
Connection con = null;
PreparedStatement pstmt = null;
if (historyDays > 0)
{
final Date deleteBefore;
deleteBefore = new Date(System.currentTimeMillis() - historyDays * 24L * 60L * 60L * 1000L);
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_OLD_USER_STATUS_HISTORY);
pstmt.setString(1, StringUtils.dateToMillis(deleteBefore));
pstmt.executeUpdate();
}
catch (SQLException e)
{
Log.error("Unable to delete old user status history", e);
}
finally
{
DbConnectionManager.closeConnection(pstmt, con);
}
}
}
private String getHostAddress(Session session)
{
try
{
return session.getHostAddress();
}
catch (UnknownHostException e)
{
return "";
}
}
}
[/code]
代码中 session.getAddress().getNode());
的意思是获得当前的XMPP用户名。XMPP协议规定,用户名分三部份:
someone @ domain.tld / clientname
^ ^ ^
用户名node 域domain 客户端名字,又叫资源resource,用于区分一个ID多处同时登录
也就是说,用户在XMPP客户端更新了自己的status,Openfire会调用http://www2.dormforce.net/services/callhub.php?SynXPwd=___&username=%s&status=%s
一次。可以把这个地址理解为一个WebService。例如我我们DormForce的官方客户端里更新我的状态
Openfire收到后会自动调用这个网址:http://www2.dormforce.net/services/callhub.php?SynXPwd=___&username=est&status=%E5%8F%AF%E4%BB%A5%E6%8A%8A%4E%55%54%61%6C%6B%E5%BD%93%74%77%69%74%74%65%72%E7%94%A8%E4%BA%86
那么我们在callhub.php
里编写任意代码了,比如我们可以自己实现一个类似Twitter的应用,也可以直接调用API把消息发送到真实的Twitter或则国内的fanfou
编译
首先删除 /opt/openfire_src/src/plugins/
下的不需要的插件,然后把user-status文件夹上传到plugins目录下
把user-status/WEB-INF
复制到根 / 。也就是 /WEB-INF/ 。不要问我为什么,我也不知道为什么,反正不这么做就是编译不成功。
然后cd /opt/openfire_src/build
ant clean && ant plugins
几分钟过后如果显示BUILD SUCCESSFUL那么说明编译成功了!
部署插件
编译好的插件在哪里呢?在/opt/openfire_src/target/openfire/plugins/user-status.jar
上传user-status.jar
到openfire的安装目录下的plugins
文件夹,到Openfire管理界面里看看,如果这个插件已经启用,那么说明我们的twitter平台已经搭建起来了
参考
基本是一些e文文档
Openfire接口文档 http://www.igniterealtime.org/builds/openfire/docs/latest/documentation/javadoc/index.html
JavaDoc 1.5 http://java.sun.com/j2se/1.5.0/docs/api/
user-status插件及其源码 http://maven.reucon.com/public/com/reucon/openfire/plugins/user-status/1.0.2/
Openfire开发指南 http://www.igniterealtime.org/projects/openfire/documentation.jsp