/*
* Copyright 2007-2010 WorldWide Conferencing, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.liftweb {
package xmpp {
import _root_.java.util.Collection
import _root_.java.io.IOException
import _root_.org.jivesoftware.smack.Chat
import _root_.org.jivesoftware.smack.ChatManager
import _root_.org.jivesoftware.smack.ConnectionConfiguration
import _root_.org.jivesoftware.smack.MessageListener
import _root_.org.jivesoftware.smack.ChatManagerListener
import _root_.org.jivesoftware.smack.Roster
import _root_.org.jivesoftware.smack.RosterEntry
import _root_.org.jivesoftware.smack.RosterListener
import _root_.org.jivesoftware.smack.XMPPConnection
import _root_.org.jivesoftware.smack.XMPPException
import _root_.org.jivesoftware.smack.packet.Message
import _root_.org.jivesoftware.smack.packet.Presence
import _root_.org.jivesoftware.smack.util.StringUtils
import _root_.net.liftweb.actor._
import _root_.scala.collection.mutable.HashMap
import _root_.scala.collection.mutable.Map
/** These messages are sent to the XMPPDispatcher Actor. */
// Send the Presence to the XMPP server
case class SetPresence(presence: Presence)
case class CreateChat(to: String)
case class SendMsg(to: String, msg: String)
case class CloseChat(to: String)
case class GetPendingMsg(to: String)
/** These messages are sent to the client Actor */
case class NewRoster(r: Roster)
// TODO(stevej): make these type-safe when Java generics are in.
case class RosterEntriesDeleted[T](entries: Collection[T])
case class RosterEntriesUpdated[T](entries: Collection[T])
case class RosterEntriesAdded[T](entries: Collection[T])
case class RosterPresenceChanged(p: Presence)
case class NewChat(chat: Chat)
case class RecvMsg(chat: Chat, msg: Message)
case class BulkMsg(chat: Chat, msg: List[Message])
// A RosterListener that sends events to the Actor given.
abstract class DispatchRosterListener(val dispatch: LiftActor) extends RosterListener
/**
* An XMPP Dispatcher connects to an XMPP server on behalf of a User.
*
*
* @param connf A function that returns the proper ConnectionConfiguration
* @param login A function that takes an XMPPConnection and initializes the connection
* by logging in.
* @author Steve Jenson (stevej@pobox.com)
*/
class XMPPDispatcher(val connf: () => ConnectionConfiguration, val login: XMPPConnection => Unit) extends LiftActor {
val conn = new XMPPConnection(connf())
conn.connect
login(conn)
val roster: Roster = conn.getRoster();
// Some XMPP server configs do not give you a Roster.
if (roster != null) {
roster.addRosterListener(new DispatchRosterListener(this) {
def entriesDeleted(a: Collection[String]) {
dispatch ! RosterEntriesDeleted(a)
}
def entriesUpdated(a: Collection[String]) {
dispatch ! RosterEntriesUpdated(a)
}
def entriesAdded(a: Collection[String]) {
dispatch ! RosterEntriesAdded(a)
}
def presenceChanged(p: Presence) {
dispatch ! RosterPresenceChanged(p)
}
})
}
// This is a Map of to: address to Chat object.
val chats: HashMap[String, Chat] = new HashMap[String, Chat]
val pendingMsg: HashMap[String, List[String]] = new HashMap[String, List[String]]
val md = new MessageDispatcher(this)
// Manage the remotely created chats, so we don't miss incomming messages
// The only thing we need to do is add our message listener, the rest
// will be managed by the dispatching actor.
conn.getChatManager().addChatListener(new ChatManagerListener {
def chatCreated(chat: Chat, createdLocally: Boolean) {
if (!createdLocally) {
chat.addMessageListener(md)
}
}
})
private var clients: List[LiftActor] = Nil
protected def messageHandler = {
/* These are all messages we process from the client Actors. */
case AddListener(actor: LiftActor) =>
actor ! NewRoster(roster)
clients ::= actor
case RemoveListener(actor: LiftActor) =>
clients -= actor
case SetPresence(presence) => conn.sendPacket(presence)
case GetPendingMsg(to) => pendingMsg.getOrElse(to, Nil) match {
case Nil => pendingMsg -= to
case xs: List[Message] => {
pendingMsg -= to;
clients.foreach(_ ! BulkMsg(chats.getOrElse(to, null), xs))
}
case _ =>
}
case CreateChat(to) => {
val chat: Chat = conn.getChatManager().createChat(to, md)
chats += (to -> chat)
clients.foreach(_ ! NewChat(chat))
}
// Send a Message to the XMPP Server
case SendMsg(to, message) =>
val msg = new Message(to, Message.Type.chat)
msg.setBody(message)
// If there isn't an existing chat in chats, make one and put it there.
chats.getOrElse(to, Nil) match {
case chat: Chat => chat.sendMessage(msg)
case Nil => {
val chat = conn.getChatManager().createChat(to, new MessageDispatcher(this))
chats += (to -> chat)
chat.sendMessage(msg)
}
}
case CloseChat(to) => chats -= to
/* From here on are Messages we process from the XMPP server */
case r@RosterEntriesDeleted(_) => clients.foreach(_ ! r)
case r@RosterEntriesUpdated(_) => clients.foreach(_ ! r)
case r@RosterEntriesAdded(_) => clients.foreach(_ ! r)
case r@RosterPresenceChanged(_) => clients.foreach(_ ! r)
case c@NewChat(chat) => clients.foreach(_ ! c)
// A new Chat has come in from the XMPP server
case m@RecvMsg(chat, msg) => {
// If this is starting a new chat, then it won't be in the
// chats Map. So add it and send the clients a NewChat message.
chats.getOrElse(msg.getFrom(), Nil) match {
case Nil => {
chats += (msg.getFrom() -> chat)
clients.foreach(_ ! NewChat(chat))
}
case _ => {}
}
clients.foreach(_ ! RecvMsg(chat, msg))
}
case a =>
}
// Accepts messages from XMPP and sends them to the local actor for dispatching.
class MessageDispatcher(dispatch: LiftActor) extends MessageListener {
def processMessage(chat: Chat, msg: Message) {
dispatch ! RecvMsg(chat, msg)
}
}
}
case class AddListener(actor: LiftActor)
case class RemoveListener(actor: LiftActor)
case object Start
/**
* An example Chat application that prints to stdout.
*
* @param username is the username to login to at Google Talk: format: something@gmail.com
* @param password is the password for the user account at Google Talk.
*/
class ConsoleChatActor(val username: String, val password: String) extends LiftActor {
def connf() = new ConnectionConfiguration("talk.google.com", 5222, "gmail.com")
def login(conn: XMPPConnection) = conn.login(username, password)
val xmpp = new XMPPDispatcher(connf, login)
val chats: Map[String, List[Message]] = new HashMap[String, List[Message]]
val rosterMap: HashMap[String, Presence] = new HashMap[String, Presence]
var roster: Roster = null
protected def messageHandler = {
case Start => {
xmpp ! AddListener(this)
xmpp ! SetPresence(new Presence(Presence.Type.available))
}
case NewChat(c) => {
chats += (c.getParticipant -> Nil)
}
case RecvMsg(chat, msg) => {
println("RecvMsg from: " + msg.getFrom + ": " + msg.getBody);
}
case NewRoster(r) => {
println("getting a new roster: " + r)
this.roster = r
val e: Array[Object] = r.getEntries.toArray.asInstanceOf[Array[Object]]
for (entry <- e) {
val user: String = entry.asInstanceOf[RosterEntry].getUser
rosterMap += (user -> r.getPresence(user))
}
}
case RosterPresenceChanged(p) => {
val user = StringUtils.parseBareAddress(p.getFrom)
println("Roster Update: " + user + " " + p)
// It's best practice to ask the roster for the presence. This is because
// multiple presences can exist for one user and the roster knows which one
// has priority.
rosterMap += (user -> roster.getPresence(user))
}
case RosterEntriesDeleted(e) => {
println(e)
}
case RosterEntriesUpdated(e) => {
println(e)
}
case RosterEntriesAdded(e) => {
println(e)
}
case a => println(a)
}
def createChat(to: String) {
xmpp ! CreateChat(to)
}
def sendMessage(to: String, msg: String) {
xmpp ! SendMsg(to, msg)
}
/**
* @returns an Iterable of all users who aren't unavailable along with their Presence
*/
def availableUsers: Iterable[(String, Presence)] = {
rosterMap.filter((e) => e._2.getType() != Presence.Type.unavailable)
}
}
object ConsoleChatHelper {
/**
* @param u is the username
* @param p is the password
*/
def run(u: String, p: String) = {
val ex = new ConsoleChatActor(u, p)
ex ! Start
ex
}
}
}
}