Openfire是XMPP協(xié)議最好的服務器軟件。最近修改了一個插件實現(xiàn)了類Twitter功能,發(fā)出來分享
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
配置系統(tǒng)環(huán)境,聲明JDK的環(huán)境變量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.
目錄結(jié)構(gòu)有些變化,我的修改版可以在這里下載 userstatus.rar
目錄結(jié)構(gòu)是:
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./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協(xié)議規(guī)定,用戶名分三部份:
someone @ domain.tld / clientname
^ ^ ^
用戶名node 域domain 客戶端名字,又叫資源resource,用于區(qū)分一個ID多處同時登錄
也就是說,用戶在XMPP客戶端更新了自己的status,Openfire會調(diào)用http://www2./services/callhub.php?SynXPwd=___&username=%s&status=%s 一次??梢园堰@個地址理解為一個WebService。例如我我們DormForce的官方客戶端里更新我的狀態(tài)

Openfire收到后會自動調(diào)用這個網(wǎng)址:http://www2./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 里編寫任意代碼了,比如我們可以自己實現(xiàn)一個類似Twitter的應用,也可以直接調(diào)用API把消息發(fā)送到真實的Twitter或則國內(nèi)的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管理界面里看看,如果這個插件已經(jīng)啟用,那么說明我們的twitter平臺已經(jīng)搭建起來了
參考
基本是一些e文文檔
Openfire接口文檔 http://www./builds/openfire/docs/latest/documentation/javadoc/index.html
JavaDoc 1.5 http://java./j2se/1.5.0/docs/api/
user-status插件及其源碼 http://maven./public/com/reucon/openfire/plugins/user-status/1.0.2/
Openfire開發(fā)指南 http://www./projects/openfire/documentation.jsp
|