Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rudimentary MUC support #7

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ xmpp-clj allows you to write simple jabber bots in idiomatic clojure by providin
## Usage

Create a temporary jabber account for your bot. I've used gmail here, but there are a bunch of free providers
<br />
<br />

Create a leiningen project and cd into the project directory

lein new mybot
cd ./mybot
<br />
<br />

Add xmpp-clj to your deps (project.clj):

(defproject mybot "0.1.0"
:description "FIXME: write"
:dependencies [[xmpp-clj "0.3.1"]])
<br />

Open up src/mybot/core.clj and require the xmpp lib:

(ns mybot.core
Expand All @@ -47,7 +47,7 @@ example:
Next, fire up your chat client, add your new bot buddy, and send him /
her a message. The response should look someting like this:

> me: hello chatbot
> me: hello chatbot

> chatbot: Ermahgerd!!!

Expand Down Expand Up @@ -83,11 +83,11 @@ her a message. The response should look someting like this:



See the `src/xmpp_clj/examples` folder for additional examples. If
you'd like to manually manage connections, see the `xmpp-clj.bot`
namespace.
See the `src/xmpp_clj/examples` folder for additional examples,
including MUC chat. If you'd like to manually manage connections, see
the `xmpp-clj.bot` namespace.

<br />
<br />

## Problems?

Expand Down
3 changes: 2 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(defproject xmpp-clj "0.3.1"
:description "A lightweight clojure wrapper around the smack jabber/XMPP library"
:dependencies [[org.clojure/clojure "1.4.0"]
[jivesoftware/smack "3.1.0"]])
[jivesoftware/smack "3.1.0"]
[jivesoftware/smackx "3.1.0"]])


12 changes: 10 additions & 2 deletions src/xmpp_clj.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@
(defn stop-bot [& [name]]
(bot/stop (or name :default)))

(defn start-bot [& opts]
(defn start-bot-with [starter opts]
(let [opts (merge {:name :default}
(apply hash-map opts))
name (:name opts)]
(swap!
bots
(fn [bots]
(bot/stop (get bots name))
(assoc bots name (bot/start opts (get opts :handler (fn [m]))))))))
(assoc bots name (starter opts (get opts :handler (fn [m]))))))))

(defn start-bot [& opts]
(start-bot-with bot/start opts))

(defn start-bot-muc [& opts]
(start-bot-with bot/start-muc opts))



(def restart-bot start-bot)
144 changes: 104 additions & 40 deletions src/xmpp_clj/bot.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@
[org.jivesoftware.smack.packet
Message Presence Presence$Type Message$Type]
[org.jivesoftware.smack.filter MessageTypeFilter]
[org.jivesoftware.smack.util StringUtils]))
[org.jivesoftware.smack.util StringUtils]
[org.jivesoftware.smackx.muc
MultiUserChat DiscussionHistory]))

(def available-presence (Presence. Presence$Type/available))

(def chat-message-type-filter (MessageTypeFilter. Message$Type/chat))
(def groupchat-message-type-filter (MessageTypeFilter. Message$Type/groupchat))

(def no-history
(doto (DiscussionHistory.) (.setMaxChars 0)))
(def muc-join-timeout-ms (long 10000))

(defn packet-listener [conn processor]
(proxy
[PacketListener]
(proxy
[PacketListener]
[]
(processPacket [packet] (processor conn packet))))

(defn error->map [e]
(if (nil? e)
(if (nil? e)
nil
{:code (.getCode e) :message (.getMessage e)}))

Expand All @@ -29,46 +36,107 @@
:thread (.getThread m)
:from (.getFrom m)
:from-name (StringUtils/parseBareAddress (.getFrom m))
:from-nick (StringUtils/parseResource (.getFrom m))
:to (.getTo m)
:packet-id (.getPacketID m)
:error (error->map (.getError m))
:type (keyword (str (.getType m)))}
(catch Exception e (println e) {})))
:type (.getType m)}
(catch Exception e (println (str "In message->map: " e)) {})))

(defn notify-generic [conn msg-map type]
(let [mgr (.getChatManager conn)
chat (.createChat mgr (:to msg-map) nil)
msg (Message. (:to msg-map) type)]
(.setBody msg (:body msg-map))
(.sendMessage chat msg))
)

; Use this function to send a one-off notification to a user when you
; don't expect a message back.
(defn notify-one [conn msg-map]
(let [mgr (.getChatManager conn)
chat (.createChat mgr (:to msg-map) nil)
msg (Message. (:to msg-map) Message$Type/chat)]
(.setBody msg (:body msg-map))
(.sendMessage chat msg)))

; Use this function to send a one-off notification to a MUC when you
; don't expect a message back
(defn notify-muc [conn msg-map]
(let [chatroom (MultiUserChat. conn (:to msg-map))]
(.sendMessage chatroom (:body msg-map))))


(defn parse-address [from]
(try
(first (.split from "/"))
(catch Exception e (println e) from)))
(catch Exception e (println (str "In parse-address " from " : " e)) from)))

(defn create-reply [from-message-map to-message-body]
(defn create-reply [from-message-map to-message-body field]
(try
(let [to (:from from-message-map)
rep (Message.)]
(let [to (field from-message-map)
rep (Message.)]
(.setTo rep to)
(.setBody rep (str to-message-body))
(.setType rep (:type from-message-map))
rep)
(catch Exception e (println e))))
(catch Exception e (println (str "In create-reply: " e)))))


(defn reply [from-message-map to-message-body conn]
(.sendPacket conn (create-reply from-message-map to-message-body)))
(defn reply [from-message-map to-message-body conn reply-address-field]
(.sendPacket conn (create-reply from-message-map to-message-body reply-address-field)))

(defn with-message-map [handler]
(fn [conn packet]
(let [message (message->map #^Message packet)]
(try
(handler conn message)
(catch Exception e (println e))))))
(catch Exception e (println (str "In with-message-map: " e)))))))

(defn wrap-responder [handler]
(defn wrap-responder [handler reply-address-field]
(fn [conn message]
(let [resp (handler message)]
(reply message resp conn))))
(when resp
(reply message resp conn reply-address-field)))))


(defn connect
[connect-info]
(let [un (:username connect-info)
pw (:password connect-info)
host (:host connect-info)
domain (:domain connect-info)
port (get connect-info :port 5222)
connect-config (ConnectionConfiguration. host port domain)
conn (XMPPConnection. connect-config)]
(if-not (and un pw host domain)
(throw (Exception. "Required connection params not provided (:username :password :host :domain)")))
(.connect conn)
(try
(.login conn un pw)
(catch XMPPException e
(throw (Exception. (str "Couldn't log in with user's credentials: "
un
" / "
(apply str (take (count pw) (repeat "*"))))))))
(.sendPacket conn available-presence)
conn))

(defn add-listener [conn packet-processor message-type-filter response-address-field]
(.addPacketListener
conn
(packet-listener conn
(with-message-map
(wrap-responder packet-processor
response-address-field)))
message-type-filter))


(defn start
"Defines and starts an instant messaging bot that will respond to incoming
messages. `start` takes 2 parameters, the first is a map representing
messages. `start` takes 2 parameters, the first is a map representing
the data needed to make a connection to the jabber server:

connnect-info example:
{:host \"talk.google.com\"
:domain \"gmail.com\"
Expand All @@ -93,30 +161,26 @@
see javadoc for org.jivesoftware.smack.packet.Message>}
"
[connect-info packet-processor]
(let [un (:username connect-info)
pw (:password connect-info)
host (:host connect-info)
domain (:domain connect-info)
port (get connect-info :port 5222)
connect-config (ConnectionConfiguration. host port domain)
conn (XMPPConnection. connect-config)]
(if-not (and un pw host domain)
(throw (Exception. "Required connection params not provided (:username :password :host :domain)")))
(.connect conn)
(try
(.login conn un pw)
(catch XMPPException e
(throw (Exception. (str "Couldn't log in with user's credentials: "
un
" / "
(apply str (take (count pw) (repeat "*"))))))))
(.sendPacket conn available-presence)
(.addPacketListener
conn
(packet-listener conn (with-message-map (wrap-responder packet-processor)))
chat-message-type-filter)
(let [conn (connect connect-info)]
(add-listener conn packet-processor chat-message-type-filter :from)
conn))



(defn start-muc
[connect-info packet-processor]
(let [room (:room connect-info)
nick (:nick connect-info)
actual-nick (or nick (:username connect-info))]
(if-not room
(throw (Exception. "Require a room to join.")))
(let [conn (connect connect-info)
muc (MultiUserChat. conn room)]
(.join muc nick nil no-history muc-join-timeout-ms)
(add-listener conn packet-processor groupchat-message-type-filter :from-name)
conn)))


(defn stop [#^XMPPConnection conn]
(when conn
(.disconnect conn)))
26 changes: 26 additions & 0 deletions src/xmpp_clj/examples/muc.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
(ns xmpp_clj.examples.muc
(require [xmpp-clj :as xmpp]))

;; Simple jabber bot that responds to MUC chatroom messages This bot
;; will only responnd to groupchat messages, and it will only respond
;; *with* groupchat messages.
(xmpp/start-bot-muc :username "testbot@test.host"
:password "12345"
:host "test.host"
:domain "host"
:nick "testbot"
:room "chatroom@test.host"
:handler (fn [m]
;; We check that the message was
;; actually addressed to us, so we
;; don't respond to our own messages.
;; That way DoSing the chat server
;; lies.
;;
;; A falsey return value from the
;; handler function means no response
;; is sent.
(when (re-find #"^testbot:" (:body m))
(str "I'm sorry "
(:from-nick m)
", I'm afraid I can't do that."))))