summaryrefslogtreecommitdiff
path: root/jabber-muc.el
diff options
context:
space:
mode:
Diffstat (limited to 'jabber-muc.el')
-rw-r--r--jabber-muc.el1171
1 files changed, 1171 insertions, 0 deletions
diff --git a/jabber-muc.el b/jabber-muc.el
new file mode 100644
index 0000000..95066e3
--- /dev/null
+++ b/jabber-muc.el
@@ -0,0 +1,1171 @@
+;; jabber-muc.el - advanced MUC functions
+
+;; Copyright (C) 2010 - Kirill A. Korinskiy - catap@catap.ru
+;; Copyright (C) 2003, 2004, 2007, 2008, 2009, 2010 - Magnus Henoch - mange@freemail.hu
+;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net
+
+;; This file is a part of jabber.el.
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 2 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program; if not, write to the Free Software
+;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+(require 'jabber-chat)
+(require 'jabber-widget)
+(require 'jabber-disco)
+(require 'jabber-muc-nick-coloring)
+
+;; we need jabber-bookmarks for jabber-muc-autojoin (via
+;; jabber-get-bookmarks and jabber-parse-conference-bookmark):
+(require 'jabber-bookmarks)
+
+(require 'cl)
+
+;;;###autoload
+(defvar *jabber-active-groupchats* nil
+ "alist of groupchats and nicknames
+Keys are strings, the bare JID of the room.
+Values are strings.")
+
+(defvar jabber-pending-groupchats (make-hash-table)
+ "Hash table of groupchats and nicknames.
+Keys are JID symbols; values are strings.
+This table records the last nickname used to join the particular
+chat room. Items are thus never removed.")
+
+(defvar jabber-muc-participants nil
+ "alist of groupchats and participants
+Keys are strings, the bare JID of the room.
+Values are lists of nickname strings.")
+
+(defvar jabber-group nil
+ "the groupchat you are participating in")
+
+(defvar jabber-muc-topic ""
+ "The topic of the current MUC room.")
+
+(defvar jabber-role-history ()
+ "Keeps track of previously used roles")
+
+(defvar jabber-affiliation-history ()
+ "Keeps track of previously used affiliations")
+
+(defvar jabber-muc-nickname-history ()
+ "Keeps track of previously referred-to nicknames")
+
+(defcustom jabber-muc-default-nicknames nil
+ "Default nickname for specific MUC rooms."
+ :group 'jabber-chat
+ :type '(repeat
+ (cons :format "%v"
+ (string :tag "JID of room")
+ (string :tag "Nickname"))))
+
+(defcustom jabber-muc-autojoin nil
+ "List of MUC rooms to automatically join on connection.
+This list is saved in your Emacs customizations. You can also store
+such a list on the Jabber server, where it is available to every
+client; see `jabber-edit-bookmarks'."
+ :group 'jabber-chat
+ :type '(repeat (string :tag "JID of room")))
+
+(defcustom jabber-muc-disable-disco-check nil
+ "If non-nil, disable checking disco#info of rooms before joining them.
+Disco information can tell whether the room exists and whether it is
+password protected, but some servers do not support it. If you want
+to join chat rooms on such servers, set this variable to t."
+ :group 'jabber-chat
+ :type 'boolean)
+
+(defcustom jabber-groupchat-buffer-format "*-jabber-groupchat-%n-*"
+ "The format specification for the name of groupchat buffers.
+
+These fields are available (all are about the group you are chatting
+in):
+
+%n Roster name of group, or JID if no nickname set
+%b Name of group from bookmarks or roster name or JID if none set
+%j Bare JID (without resource)"
+ :type 'string
+ :group 'jabber-chat)
+
+(defcustom jabber-groupchat-prompt-format "[%t] %n> "
+ "The format specification for lines in groupchat.
+
+These fields are available:
+
+%t Time, formatted according to `jabber-chat-time-format'
+%n, %u, %r
+ Nickname in groupchat
+%j Full JID (room@server/nick)"
+ :type 'string
+ :group 'jabber-chat)
+
+(defcustom jabber-muc-header-line-format
+ '(" " (:eval (jabber-jid-displayname jabber-group))
+ "\t" jabber-muc-topic)
+ "The specification for the header line of MUC buffers.
+
+The format is that of `mode-line-format' and `header-line-format'."
+ :type 'sexp
+ :group 'jabber-chat)
+
+(defcustom jabber-muc-private-buffer-format "*-jabber-muc-priv-%g-%n-*"
+ "The format specification for the buffer name for private MUC messages.
+
+These fields are available:
+
+%g Roster name of group, or JID if no nickname set
+%n Nickname of the group member you're chatting with"
+ :type 'string
+ :group 'jabber-chat)
+
+(defcustom jabber-muc-private-foreign-prompt-format "[%t] %g/%n> "
+ "The format specification for lines others type in a private MUC buffer.
+
+These fields are available:
+
+%t Time, formatted according to `jabber-chat-time-format'
+%n Nickname in room
+%g Short room name (either roster name or username part of JID)"
+ :type 'string
+ :group 'jabber-chat)
+
+(defcustom jabber-muc-print-names-format " %n %a %j\n"
+ "The format specification for MUC list lines.
+
+Fields available:
+
+%n Nickname in room
+%a Affiliation status
+%j Full JID (room@server/nick)"
+ :type 'string
+ :group 'jabber-chat)
+
+(defcustom jabber-muc-private-header-line-format
+ '(" " (:eval (jabber-jid-resource jabber-chatting-with))
+ " in " (:eval (jabber-jid-displayname (jabber-jid-user jabber-chatting-with)))
+ "\t" jabber-events-message
+ "\t" jabber-chatstates-message)
+ "The specification for the header line of private MUC chat buffers.
+
+The format is that of `mode-line-format' and `header-line-format'."
+ :type 'sexp
+ :group 'jabber-chat)
+
+;;;###autoload
+(defvar jabber-muc-printers '()
+ "List of functions that may be able to print part of a MUC message.
+This gets prepended to `jabber-chat-printers', which see.")
+
+;;;###autoload
+(defun jabber-muc-get-buffer (group)
+ "Return the chat buffer for chatroom GROUP.
+Either a string or a buffer is returned, so use `get-buffer' or
+`get-buffer-create'."
+ (format-spec jabber-groupchat-buffer-format
+ (list
+ (cons ?n (jabber-jid-displayname group))
+ (cons ?b (jabber-jid-bookmarkname group))
+ (cons ?j (jabber-jid-user group)))))
+
+(defun jabber-muc-create-buffer (jc group)
+ "Prepare a buffer for chatroom GROUP.
+This function is idempotent."
+ (with-current-buffer (get-buffer-create (jabber-muc-get-buffer group))
+ (unless (eq major-mode 'jabber-chat-mode)
+ (jabber-chat-mode jc #'jabber-chat-pp))
+ ;; Make sure the connection variable is up to date.
+ (setq jabber-buffer-connection jc)
+
+ (set (make-local-variable 'jabber-group) group)
+ (make-local-variable 'jabber-muc-topic)
+ (setq jabber-send-function 'jabber-muc-send)
+ (setq header-line-format jabber-muc-header-line-format)
+ (current-buffer)))
+
+;;;###autoload
+(defun jabber-muc-private-get-buffer (group nickname)
+ "Return the chat buffer for private chat with NICKNAME in GROUP.
+Either a string or a buffer is returned, so use `get-buffer' or
+`get-buffer-create'."
+ (format-spec jabber-muc-private-buffer-format
+ (list
+ (cons ?g (jabber-jid-displayname group))
+ (cons ?n nickname))))
+
+(defun jabber-muc-private-create-buffer (jc group nickname)
+ "Prepare a buffer for chatting with NICKNAME in GROUP.
+This function is idempotent."
+ (with-current-buffer (get-buffer-create (jabber-muc-private-get-buffer group nickname))
+ (unless (eq major-mode 'jabber-chat-mode)
+ (jabber-chat-mode jc #'jabber-chat-pp))
+
+ (set (make-local-variable 'jabber-chatting-with) (concat group "/" nickname))
+ (setq jabber-send-function 'jabber-chat-send)
+ (setq header-line-format jabber-muc-private-header-line-format)
+
+ (current-buffer)))
+
+(defun jabber-muc-send (jc body)
+ "Send BODY to MUC room in current buffer."
+ ;; There is no need to display the sent message in the buffer, as
+ ;; we will get it back from the MUC server.
+ (jabber-send-sexp jc
+ `(message
+ ((to . ,jabber-group)
+ (type . "groupchat"))
+ (body () ,body))))
+
+(defun jabber-muc-add-groupchat (group nickname)
+ "Remember participating in GROUP under NICKNAME."
+ (let ((whichgroup (assoc group *jabber-active-groupchats*)))
+ (if whichgroup
+ (setcdr whichgroup nickname)
+ (add-to-list '*jabber-active-groupchats* (cons group nickname)))))
+
+(defun jabber-muc-remove-groupchat (group)
+ "Remove GROUP from internal bookkeeping."
+ (let ((whichgroup (assoc group *jabber-active-groupchats*))
+ (whichparticipants (assoc group jabber-muc-participants)))
+ (setq *jabber-active-groupchats*
+ (delq whichgroup *jabber-active-groupchats*))
+ (setq jabber-muc-participants
+ (delq whichparticipants jabber-muc-participants))))
+
+(defun jabber-muc-connection-closed (bare-jid)
+ "Remove MUC data for BARE-JID.
+Forget all information about rooms that had been entered with
+this JID. Suitable to call when the connection is closed."
+ (dolist (room-entry jabber-muc-participants)
+ (let* ((room (car room-entry))
+ (buffer (get-buffer (jabber-muc-get-buffer room))))
+ (when (bufferp buffer)
+ (with-current-buffer buffer
+ (when (string= bare-jid
+ (jabber-connection-bare-jid jabber-buffer-connection))
+ (setq *jabber-active-groupchats*
+ (delete* room *jabber-active-groupchats*
+ :key #'car :test #'string=))
+ (setq jabber-muc-participants
+ (delq room-entry jabber-muc-participants))))))))
+
+(defun jabber-muc-participant-plist (group nickname)
+ "Return plist associated with NICKNAME in GROUP.
+Return nil if nothing known about that combination."
+ (let ((whichparticipants (assoc group jabber-muc-participants)))
+ (when whichparticipants
+ (cdr (assoc nickname whichparticipants)))))
+
+(defun jabber-muc-modify-participant (group nickname new-plist)
+ "Assign properties in NEW-PLIST to NICKNAME in GROUP."
+ (let ((participants (assoc group jabber-muc-participants)))
+ ;; either we have a list of participants already...
+ (if participants
+ (let ((participant (assoc nickname participants)))
+ ;; and maybe this participant is already in the list
+ (if participant
+ ;; if so, just update role, affiliation, etc.
+ (setf (cdr participant) new-plist)
+ (push (cons nickname new-plist) (cdr participants))))
+ ;; or we don't
+ (push (cons group (list (cons nickname new-plist))) jabber-muc-participants))))
+
+(defun jabber-muc-report-delta (nickname old-plist new-plist reason actor)
+ "Compare OLD-PLIST and NEW-PLIST, and return a string explaining the change.
+Return nil if nothing noteworthy has happened.
+NICKNAME is the user experiencing the change. REASON and ACTOR, if non-nil,
+are the corresponding presence fields.
+
+This function is only concerned with presence stanzas resulting
+in the user entering/staying in the room."
+ ;; The keys in the plist are affiliation, role and jid.
+ (when (plist-get new-plist 'jid)
+ ;; nickname is only used for displaying, so we can modify it if we
+ ;; want to.
+ (setq nickname (concat nickname " <"
+ (jabber-jid-user (plist-get new-plist 'jid))
+ ">")))
+ (cond
+ ((null old-plist)
+ ;; User enters the room
+ (concat nickname " enters the room ("
+ (plist-get new-plist 'role)
+ (unless (string= (plist-get new-plist 'affiliation) "none")
+ (concat ", " (plist-get new-plist 'affiliation)))
+ ")"))
+
+ ;; If affiliation changes, the role change is usually the logical
+ ;; one, so don't report it separately.
+ ((not (string= (plist-get old-plist 'affiliation)
+ (plist-get new-plist 'affiliation)))
+ (let ((actor-reason (concat (when actor
+ (concat " by " actor))
+ (when reason
+ (concat ": " reason))))
+ (from (plist-get old-plist 'affiliation))
+ (to (plist-get new-plist 'affiliation)))
+ ;; There are many ways to express these transitions in English.
+ ;; This one favors eloquence over regularity and consistency.
+ (cond
+ ;; Higher affiliation
+ ((or (and (member from '("outcast" "none" "member"))
+ (member to '("admin" "owner")))
+ (and (string= from "admin") (string= to "owner")))
+ (concat nickname " has been promoted to " to actor-reason))
+ ;; Lower affiliation
+ ((or (and (member from '("owner" "admin"))
+ (string= to "member"))
+ (and (string= from "owner") (string= to "admin")))
+ (concat nickname " has been demoted to " to actor-reason))
+ ;; Become member
+ ((string= to "member")
+ (concat nickname " has been granted membership" actor-reason))
+ ;; Lose membership
+ ((string= to "none")
+ (concat nickname " has been deprived of membership" actor-reason)))))
+
+ ;; Role changes
+ ((not (string= (plist-get old-plist 'role)
+ (plist-get new-plist 'role)))
+ (let ((actor-reason (concat (when actor
+ (concat " by " actor))
+ (when reason
+ (concat ": " reason))))
+ (from (plist-get old-plist 'role))
+ (to (plist-get new-plist 'role)))
+ ;; Possible roles are "none" (not in room, hence not of interest
+ ;; in this function), "visitor" (no voice), "participant" (has
+ ;; voice), and "moderator".
+ (cond
+ ((string= to "moderator")
+ (concat nickname " has been granted moderator privileges" actor-reason))
+ ((and (string= from "moderator")
+ (string= to "participant"))
+ (concat nickname " had moderator privileges revoked" actor-reason))
+ ((string= to "participant")
+ (concat nickname " has been granted voice" actor-reason))
+ ((string= to "visitor")
+ (concat nickname " has been denied voice" actor-reason)))))))
+
+(defun jabber-muc-remove-participant (group nickname)
+ "Forget everything about NICKNAME in GROUP."
+ (let ((participants (assoc group jabber-muc-participants)))
+ (when participants
+ (let ((participant (assoc nickname (cdr participants))))
+ (setf (cdr participants) (delq participant (cdr participants)))))))
+
+(defmacro jabber-muc-argument-list (&optional args)
+ "Prepend connection and group name to ARGS.
+If the current buffer is not an MUC buffer, signal an error.
+This macro is meant for use as an argument to `interactive'."
+ `(if (null jabber-group)
+ (error "Not in MUC buffer")
+ (nconc (list jabber-buffer-connection jabber-group) ,args)))
+
+(defun jabber-muc-read-completing (prompt &optional allow-not-joined)
+ "Read the name of a joined chatroom, or use chatroom of current buffer, if any.
+If ALLOW-NOT-JOINED is provided and true, permit choosing any
+JID; only provide completion as a guide."
+ (or jabber-group
+ (jabber-read-jid-completing prompt
+ (if (null *jabber-active-groupchats*)
+ (error "You haven't joined any group")
+ (mapcar (lambda (x) (jabber-jid-symbol (car x)))
+ *jabber-active-groupchats*))
+ (not allow-not-joined)
+ jabber-group)))
+
+(defun jabber-muc-read-nickname (group prompt)
+ "Read the nickname of a participant in GROUP."
+ (let ((nicknames (cdr (assoc group jabber-muc-participants))))
+ (unless nicknames
+ (error "Unknown group: %s" group))
+ (completing-read prompt nicknames nil t nil 'jabber-muc-nickname-history)))
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Request vcard" 'jabber-muc-vcard-get))
+
+;;;###autoload
+(defun jabber-muc-vcard-get (jc group nickname)
+ "Request vcard from chat with NICKNAME in GROUP."
+ (interactive
+ (jabber-muc-argument-list
+ (list (jabber-muc-read-nickname jabber-group "Nickname: "))))
+ (let ((muc-name (format "%s/%s" group nickname)))
+ (jabber-vcard-get jc muc-name)))
+
+(defun jabber-muc-instant-config (jc group)
+ "Accept default configuration for GROUP.
+This can be used for a newly created room, as an alternative to
+filling out the configuration form with `jabber-muc-get-config'.
+Both of these methods unlock the room, so that other users can
+enter it."
+ (interactive (jabber-muc-argument-list))
+ (jabber-send-iq jc group
+ "set"
+ '(query ((xmlns . "http://jabber.org/protocol/muc#owner"))
+ (x ((xmlns . "jabber:x:data") (type . "submit"))))
+ #'jabber-report-success "MUC instant configuration"
+ #'jabber-report-success "MUC instant configuration"))
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Configure groupchat" 'jabber-muc-get-config))
+
+(defun jabber-muc-get-config (jc group)
+ "Ask for MUC configuration form"
+ (interactive (jabber-muc-argument-list))
+ (jabber-send-iq jc group
+ "get"
+ '(query ((xmlns . "http://jabber.org/protocol/muc#owner")))
+ #'jabber-process-data #'jabber-muc-render-config
+ #'jabber-process-data "MUC configuration request failed"))
+
+(defalias 'jabber-groupchat-get-config 'jabber-muc-get-config
+ "Deprecated. See `jabber-muc-get-config' instead.")
+
+(defun jabber-muc-render-config (jc xml-data)
+ "Render MUC configuration form"
+
+ (let ((query (jabber-iq-query xml-data))
+ xdata)
+ (dolist (x (jabber-xml-get-children query 'x))
+ (if (string= (jabber-xml-get-attribute x 'xmlns) "jabber:x:data")
+ (setq xdata x)))
+ (if (not xdata)
+ (insert "No configuration possible.\n")
+
+ (jabber-init-widget-buffer (jabber-xml-get-attribute xml-data 'from))
+ (setq jabber-buffer-connection jc)
+
+ (jabber-render-xdata-form xdata)
+
+ (widget-create 'push-button :notify #'jabber-muc-submit-config "Submit")
+ (widget-insert "\t")
+ (widget-create 'push-button :notify #'jabber-muc-cancel-config "Cancel")
+ (widget-insert "\n")
+
+ (widget-setup)
+ (widget-minor-mode 1))))
+
+(defalias 'jabber-groupchat-render-config 'jabber-muc-render-config
+ "Deprecated. See `jabber-muc-render-config' instead.")
+
+(defun jabber-muc-submit-config (&rest ignore)
+ "Submit MUC configuration form."
+
+ (jabber-send-iq jabber-buffer-connection jabber-submit-to
+ "set"
+ `(query ((xmlns . "http://jabber.org/protocol/muc#owner"))
+ ,(jabber-parse-xdata-form))
+ #'jabber-report-success "MUC configuration"
+ #'jabber-report-success "MUC configuration"))
+
+(defalias 'jabber-groupchat-submit-config 'jabber-muc-submit-config
+ "Deprecated. See `jabber-muc-submit-config' instead.")
+
+(defun jabber-muc-cancel-config (&rest ignore)
+ "Cancel MUC configuration form."
+
+ (jabber-send-iq jabber-buffer-connection jabber-submit-to
+ "set"
+ '(query ((xmlns . "http://jabber.org/protocol/muc#owner"))
+ (x ((xmlns . "jabber:x:data") (type . "cancel"))))
+ nil nil nil nil))
+
+(defalias 'jabber-groupchat-cancel-config 'jabber-muc-cancel-config
+ "Deprecated. See `jabber-muc-cancel-config' instead.")
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Join groupchat" 'jabber-muc-join))
+
+(defun jabber-muc-join (jc group nickname &optional popup)
+ "join a groupchat, or change nick.
+In interactive calls, or if POPUP is true, switch to the
+groupchat buffer."
+ (interactive
+ (let ((account (jabber-read-account))
+ (group (jabber-read-jid-completing "group: ")))
+ (list account group (jabber-muc-read-my-nickname account group) t)))
+
+ ;; If the user is already in the room, we don't need as many checks.
+ (if (or (assoc group *jabber-active-groupchats*)
+ ;; Or if the users asked us not to check disco info.
+ jabber-muc-disable-disco-check)
+ (jabber-muc-join-3 jc group nickname nil popup)
+ ;; Else, send a disco request to find out what we are connecting
+ ;; to.
+ (jabber-disco-get-info jc group nil #'jabber-muc-join-2
+ (list group nickname popup))))
+
+(defalias 'jabber-groupchat-join 'jabber-muc-join
+ "Deprecated. Use `jabber-muc-join' instead.")
+
+(defun jabber-muc-join-2 (jc closure result)
+ (destructuring-bind (group nickname popup) closure
+ (let* ( ;; Either success...
+ (identities (car result))
+ (features (cadr result))
+ ;; ...or error
+ (condition (when (eq identities 'error) (jabber-error-condition result))))
+ (cond
+ ;; Maybe the room doesn't exist yet.
+ ((eq condition 'item-not-found)
+ (unless (or jabber-silent-mode
+ (y-or-n-p (format "%s doesn't exist. Create it? "
+ (jabber-jid-displayname group))))
+ (error "Non-existent groupchat")))
+
+ ;; Maybe the room doesn't support disco.
+ ((eq condition 'feature-not-implemented)
+ t ;whatever... we will ignore it later
+ )
+ ;; Maybe another error occurred. Report it to user
+ (condition
+ (message "Couldn't query groupchat: %s" (jabber-parse-error result)))
+
+ ;; Bad stanza? Without NS, for example
+ ((and (eq identities 'error) (not condition))
+ (message "Bad error stanza received")))
+
+ ;; Continue only if it is really chat room. If there was an
+ ;; error, give the chat room the benefit of the doubt. (Needed
+ ;; for ejabberd's mod_irc, for example)
+ (when (or condition
+ (find "conference" (if (sequencep identities) identities nil)
+ :key (lambda (i) (aref i 1))
+ :test #'string=))
+ (let ((password
+ ;; Is the room password-protected?
+ (when (member "muc_passwordprotected" features)
+ (or
+ (jabber-get-conference-data jc group nil :password)
+ (read-passwd (format "Password for %s: " (jabber-jid-displayname group)))))))
+
+ (jabber-muc-join-3 jc group nickname password popup))))))
+
+(defalias 'jabber-groupchat-join-2 'jabber-muc-join-2
+ "Deprecated. See `jabber-muc-join-2' instead.")
+
+(defun jabber-muc-join-3 (jc group nickname password popup)
+
+ ;; Remember that this is a groupchat _before_ sending the stanza.
+ ;; The response might come quicker than you think.
+
+ (puthash (jabber-jid-symbol group) nickname jabber-pending-groupchats)
+
+ (jabber-send-sexp jc
+ `(presence ((to . ,(format "%s/%s" group nickname)))
+ (x ((xmlns . "http://jabber.org/protocol/muc"))
+ ,@(when password
+ `((password () ,password))))
+ ,@(jabber-presence-children jc)))
+
+ ;; There, stanza sent. Now we just wait for the MUC service to
+ ;; mirror the stanza. This is handled in
+ ;; `jabber-muc-process-presence', where a buffer will be created for
+ ;; the room.
+
+ ;; But if the user interactively asked to join, he/she probably
+ ;; wants the buffer to pop up right now.
+ (when popup
+ (let ((buffer (jabber-muc-create-buffer jc group)))
+ (switch-to-buffer buffer))))
+
+(defalias 'jabber-groupchat-join-3 'jabber-muc-join-3
+ "Deprecated. See `jabber-muc-join-3' instead.")
+
+(defun jabber-muc-read-my-nickname (jc group &optional default)
+ "Read nickname for joining GROUP. If DEFAULT is non-nil, return default nick without prompting."
+ (let ((default-nickname (or
+ (jabber-get-conference-data jc group nil :nick)
+ (cdr (assoc group jabber-muc-default-nicknames))
+ (plist-get (fsm-get-state-data jc) :username))))
+ (if default
+ default-nickname
+ (jabber-read-with-input-method (format "Nickname: (default %s) "
+ default-nickname)
+ nil nil default-nickname))))
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Change nickname" 'jabber-muc-nick))
+
+(defalias 'jabber-muc-nick 'jabber-muc-join)
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Leave groupchat" 'jabber-muc-leave))
+
+(defun jabber-muc-leave (jc group)
+ "leave a groupchat"
+ (interactive (jabber-muc-argument-list))
+ (let ((whichgroup (assoc group *jabber-active-groupchats*)))
+ ;; send unavailable presence to our own nick in room
+ (jabber-send-sexp jc
+ `(presence ((to . ,(format "%s/%s" group (cdr whichgroup)))
+ (type . "unavailable"))))))
+
+(defalias 'jabber-groupchat-leave 'jabber-muc-leave
+ "Deprecated. Use `jabber-muc-leave' instead.")
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "List participants" 'jabber-muc-names))
+
+(defun jabber-muc-names ()
+ "Print names, affiliations, and roles of participants in current buffer."
+ (interactive)
+ (ewoc-enter-last jabber-chat-ewoc (list :notice
+ (jabber-muc-print-names
+ (cdr (assoc jabber-group jabber-muc-participants)))
+ :time (current-time))))
+
+(defun jabber-muc-format-names (participant)
+ "Format one participant name"
+ (format-spec jabber-muc-print-names-format
+ (list
+ (cons ?n (car participant))
+ (cons ?a (plist-get (cdr participant) 'affiliation))
+ (cons ?j (or (plist-get (cdr participant) 'jid) "")))))
+
+(defun jabber-muc-print-names (participants)
+ "Format and return data in PARTICIPANTS."
+ (let ((mlist) (plist) (vlist) (nlist))
+ (mapcar (lambda (x)
+ (let ((role (plist-get (cdr x) 'role)))
+ (cond ((string= role "moderator")
+ (add-to-list 'mlist x))
+ ((string= role "participant")
+ (add-to-list 'plist x))
+ ((string= role "visitor")
+ (add-to-list 'vlist x))
+ ((string= role "none")
+ (add-to-list 'nlist x)))))
+ participants)
+ (concat
+ (apply 'concat "\nModerators:\n" (mapcar 'jabber-muc-format-names mlist))
+ (apply 'concat "\nParticipants:\n" (mapcar 'jabber-muc-format-names plist))
+ (apply 'concat "\nVisitors:\n" (mapcar 'jabber-muc-format-names vlist))
+ (apply 'concat "\nNones:\n" (mapcar 'jabber-muc-format-names nlist)))
+ ))
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Set topic" 'jabber-muc-set-topic))
+
+(defun jabber-muc-set-topic (jc group topic)
+ "Set topic of GROUP to TOPIC."
+ (interactive
+ (jabber-muc-argument-list
+ (list (jabber-read-with-input-method "New topic: " jabber-muc-topic))))
+ (jabber-send-message jc group topic nil "groupchat"))
+
+(defun jabber-muc-snarf-topic (xml-data)
+ "Record subject (topic) of the given <message/>, if any."
+ (let ((new-topic (jabber-xml-path xml-data '(subject ""))))
+ (when new-topic
+ (setq jabber-muc-topic new-topic))))
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Set role (kick, voice, op)" 'jabber-muc-set-role))
+
+(defun jabber-muc-set-role (jc group nickname role reason)
+ "Set role of NICKNAME in GROUP to ROLE, specifying REASON."
+ (interactive
+ (jabber-muc-argument-list
+ (let ((nickname (jabber-muc-read-nickname jabber-group "Nickname: ")))
+ (list nickname
+ (completing-read "New role: " '(("none") ("visitor") ("participant") ("moderator")) nil t nil 'jabber-role-history)
+ (read-string "Reason: ")))))
+ (unless (or (zerop (length nickname)) (zerop (length role)))
+ (jabber-send-iq jc group "set"
+ `(query ((xmlns . "http://jabber.org/protocol/muc#admin"))
+ (item ((nick . ,nickname)
+ (role . ,role))
+ ,(unless (zerop (length reason))
+ `(reason () ,reason))))
+ 'jabber-report-success "Role change"
+ 'jabber-report-success "Role change")))
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Set affiliation (ban, member, admin)" 'jabber-muc-set-affiliation))
+
+(defun jabber-muc-set-affiliation (jc group nickname-or-jid nickname-p affiliation reason)
+ "Set affiliation of NICKNAME-OR-JID in GROUP to AFFILIATION.
+If NICKNAME-P is non-nil, NICKNAME-OR-JID is a nickname in the
+group, else it is a JID."
+ (interactive
+ (jabber-muc-argument-list
+ (let ((nickname-p (y-or-n-p "Specify user by room nickname? ")))
+ (list
+ (if nickname-p
+ (jabber-muc-read-nickname jabber-group "Nickname: ")
+ (jabber-read-jid-completing "User: "))
+ nickname-p
+ (completing-read "New affiliation: "
+ '(("none") ("outcast") ("member") ("admin") ("owner")) nil t nil 'jabber-affiliation-history)
+ (read-string "Reason: ")))))
+ (let ((jid
+ (if nickname-p
+ (let ((participants (cdr (assoc group jabber-muc-participants))))
+ (unless participants
+ (error "Couldn't find group %s" group))
+ (let ((participant (cdr (assoc nickname-or-jid participants))))
+ (unless participant
+ (error "Couldn't find %s in group %s" nickname-or-jid group))
+ (or (plist-get participant 'jid)
+ (error "JID of %s in group %s is unknown" nickname-or-jid group))))
+ nickname-or-jid)))
+ (jabber-send-iq jc group "set"
+ `(query ((xmlns . "http://jabber.org/protocol/muc#admin"))
+ (item ((jid . ,jid)
+ (affiliation . ,affiliation))
+ ,(unless (zerop (length reason))
+ `(reason () ,reason))))
+ 'jabber-report-success "Affiliation change"
+ 'jabber-report-success "Affiliation change")))
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Invite someone to chatroom" 'jabber-muc-invite))
+
+(defun jabber-muc-invite (jc jid group reason)
+ "Invite JID to GROUP, stating REASON."
+ (interactive
+ (list (jabber-read-account)
+ (jabber-read-jid-completing
+ "Invite whom: "
+ ;; The current room is _not_ a good default for whom to invite.
+ (remq (jabber-jid-symbol jabber-group) (jabber-concat-rosters)))
+ (jabber-muc-read-completing "To group: ")
+ (jabber-read-with-input-method "Reason: ")))
+ (jabber-send-sexp
+ jc
+ `(message ((to . ,group))
+ (x ((xmlns . "http://jabber.org/protocol/muc#user"))
+ (invite ((to . ,jid))
+ ,(unless (zerop (length reason))
+ `(reason nil ,reason)))))))
+
+(add-to-list 'jabber-body-printers 'jabber-muc-print-invite)
+
+(defun jabber-muc-print-invite (xml-data who mode)
+ "Print MUC invitation"
+ (dolist (x (jabber-xml-get-children xml-data 'x))
+ (when (string= (jabber-xml-get-attribute x 'xmlns) "http://jabber.org/protocol/muc#user")
+ (let ((invitation (car (jabber-xml-get-children x 'invite))))
+ (when invitation
+ (when (eql mode :insert)
+ (let ((group (jabber-xml-get-attribute xml-data 'from))
+ (inviter (jabber-xml-get-attribute invitation 'from))
+ (reason (car (jabber-xml-node-children (car (jabber-xml-get-children invitation 'reason))))))
+ ;; XXX: password
+ (insert "You have been invited to MUC room " (jabber-jid-displayname group))
+ (when inviter
+ (insert " by " (jabber-jid-displayname inviter)))
+ (insert ".")
+ (when reason
+ (insert " Reason: " reason))
+ (insert "\n\n")
+
+ (let ((action
+ `(lambda (&rest ignore) (interactive)
+ (jabber-muc-join jabber-buffer-connection ,group
+ (jabber-muc-read-my-nickname jabber-buffer-connection ,group)))))
+ (if (fboundp 'insert-button)
+ (insert-button "Accept"
+ 'action action)
+ ;; Simple button replacement
+ (let ((keymap (make-keymap)))
+ (define-key keymap "\r" action)
+ (insert (jabber-propertize "Accept"
+ 'keymap keymap
+ 'face 'highlight))))
+
+ (insert "\t")
+
+ (let ((action
+ `(lambda (&rest ignore) (interactive)
+ (let ((reason
+ (jabber-read-with-input-method
+ "Reason: ")))
+ (jabber-send-sexp
+ jabber-buffer-connection
+ (list 'message
+ (list (cons 'to ,group))
+ (list 'x
+ (list (cons 'xmlns "http://jabber.org/protocol/muc#user"))
+ (list 'decline
+ (list (cons 'to ,inviter))
+ (unless (zerop (length reason))
+ (list 'reason nil reason))))))))))
+ (if (fboundp 'insert-button)
+ (insert-button "Decline"
+ 'action action)
+ ;; Simple button replacement
+ (let ((keymap (make-keymap)))
+ (define-key keymap "\r" action)
+ (insert (jabber-propertize "Decline"
+ 'keymap keymap
+ 'face 'highlight))))))))
+ (return t))))))
+
+(defun jabber-muc-autojoin (jc)
+ "Join rooms specified in account bookmarks and global `jabber-muc-autojoin'."
+ (interactive (list (jabber-read-account)))
+ (let ((nickname (plist-get (fsm-get-state-data jc) :username)))
+ (when (bound-and-true-p jabber-muc-autojoin)
+ (dolist (group jabber-muc-autojoin)
+ (jabber-muc-join jc group (or
+ (cdr (assoc group jabber-muc-default-nicknames))
+ (plist-get (fsm-get-state-data jc) :username)))))
+ (jabber-get-bookmarks
+ jc
+ (lambda (jc bookmarks)
+ (dolist (bookmark bookmarks)
+ (setq bookmark (jabber-parse-conference-bookmark bookmark))
+ (when (and bookmark (plist-get bookmark :autojoin))
+ (jabber-muc-join jc (plist-get bookmark :jid)
+ (or (plist-get bookmark :nick)
+ (plist-get (fsm-get-state-data jc) :username)))))))))
+
+;;;###autoload
+(defun jabber-muc-message-p (message)
+ "Return non-nil if MESSAGE is a groupchat message.
+That does not include private messages in a groupchat, but does
+include groupchat invites."
+ ;; Public groupchat messages have type "groupchat" and are from
+ ;; room@server/nick. Public groupchat errors have type "error" and
+ ;; are from room@server.
+ (let ((from (jabber-xml-get-attribute message 'from))
+ (type (jabber-xml-get-attribute message 'type)))
+ (or
+ (string= type "groupchat")
+ (and (string= type "error")
+ (gethash (jabber-jid-symbol from) jabber-pending-groupchats))
+ (jabber-xml-path message '(("http://jabber.org/protocol/muc#user" . "x") invite)))))
+
+;;;###autoload
+(defun jabber-muc-sender-p (jid)
+ "Return non-nil if JID is a full JID of an MUC participant."
+ (and (assoc (jabber-jid-user jid) *jabber-active-groupchats*)
+ (jabber-jid-resource jid)))
+
+;;;###autoload
+(defun jabber-muc-private-message-p (message)
+ "Return non-nil if MESSAGE is a private message in a groupchat."
+ (let ((from (jabber-xml-get-attribute message 'from))
+ (type (jabber-xml-get-attribute message 'type)))
+ (and
+ (not (string= type "groupchat"))
+ (jabber-muc-sender-p from))))
+
+(add-to-list 'jabber-jid-muc-menu
+ (cons "Open private chat" 'jabber-muc-private))
+
+(defun jabber-muc-private (jc group nickname)
+ "Open private chat with NICKNAME in GROUP."
+ (interactive
+ (jabber-muc-argument-list
+ (list (jabber-muc-read-nickname jabber-group "Nickname: "))))
+ (switch-to-buffer (jabber-muc-private-create-buffer jabber-buffer-connection group nickname)))
+
+(defun jabber-muc-presence-p (presence)
+ "Return non-nil if PRESENCE is presence from groupchat."
+ (let ((from (jabber-xml-get-attribute presence 'from))
+ (type (jabber-xml-get-attribute presence 'type))
+ (muc-marker (find-if
+ (lambda (x) (equal (jabber-xml-get-attribute x 'xmlns)
+ "http://jabber.org/protocol/muc#user"))
+ (jabber-xml-get-children presence 'x))))
+ ;; This is MUC presence if it has an MUC-namespaced tag...
+ (or muc-marker
+ ;; ...or if it is error presence from a room we tried to join.
+ (and (string= type "error")
+ (gethash (jabber-jid-symbol from) jabber-pending-groupchats)))))
+
+(defun jabber-muc-parse-affiliation (x-muc)
+ "Parse X-MUC in the muc#user namespace and return a plist.
+Return nil if X-MUC is nil."
+ ;; XXX: parse <actor/> and <reason/> tags? or maybe elsewhere?
+ (apply 'nconc (mapcar (lambda (prop) (list (car prop) (cdr prop)))
+ (jabber-xml-node-attributes
+ (car (jabber-xml-get-children x-muc 'item))))))
+
+(defun jabber-muc-print-prompt (xml-data &optional local dont-print-nick-p)
+ "Print MUC prompt for message in XML-DATA."
+ (let ((nick (jabber-jid-resource (jabber-xml-get-attribute xml-data 'from)))
+ (timestamp (jabber-message-timestamp xml-data)))
+ (if (stringp nick)
+ (insert (jabber-propertize
+ (format-spec jabber-groupchat-prompt-format
+ (list
+ (cons ?t (format-time-string
+ (if timestamp
+ jabber-chat-delayed-time-format
+ jabber-chat-time-format)
+ timestamp))
+ (cons ?n (if dont-print-nick-p "" nick))
+ (cons ?u nick)
+ (cons ?r nick)
+ (cons ?j (concat jabber-group "/" nick))))
+ 'face (if local ;Message from you.
+ (if jabber-muc-colorize-local ;; If colorization enable...
+ ;; ...colorize nick
+ (list ':foreground (jabber-muc-nick-get-color nick))
+ ;; otherwise, use default face.
+ 'jabber-chat-prompt-local)
+ ;; Message from other participant.
+ (if jabber-muc-colorize-foreign ;If colorization enable...
+ ;; ... colorize nick
+ (list ':foreground (jabber-muc-nick-get-color nick))
+ ;; otherwise, use default face.
+ 'jabber-chat-prompt-foreign))
+ 'help-echo (concat (format-time-string "On %Y-%m-%d %H:%M:%S" timestamp) " from " nick " in " jabber-group)))
+ (jabber-muc-system-prompt))))
+
+(defun jabber-muc-private-print-prompt (xml-data)
+ "Print prompt for private MUC message in XML-DATA."
+ (let ((nick (jabber-jid-resource (jabber-xml-get-attribute xml-data 'from)))
+ (group (jabber-jid-user (jabber-xml-get-attribute xml-data 'from)))
+ (timestamp (jabber-message-timestamp xml-data)))
+ (insert (jabber-propertize
+ (format-spec jabber-muc-private-foreign-prompt-format
+ (list
+ (cons ?t (format-time-string
+ (if timestamp
+ jabber-chat-delayed-time-format
+ jabber-chat-time-format)
+ timestamp))
+ (cons ?n nick)
+ (cons ?g (or (jabber-jid-rostername group)
+ (jabber-jid-username group)))))
+ 'face 'jabber-chat-prompt-foreign
+ 'help-echo (concat (format-time-string "On %Y-%m-%d %H:%M:%S" timestamp) " from " nick " in " jabber-group)))))
+
+(defun jabber-muc-system-prompt (&rest ignore)
+ "Print system prompt for MUC."
+ (insert (jabber-propertize
+ (format-spec jabber-groupchat-prompt-format
+ (list
+ (cons ?t (format-time-string jabber-chat-time-format))
+ (cons ?n "")
+ (cons ?u "")
+ (cons ?r "")
+ (cons ?j jabber-group)))
+ 'face 'jabber-chat-prompt-system
+ 'help-echo (format-time-string "System message on %Y-%m-%d %H:%M:%S"))))
+
+(add-to-list 'jabber-message-chain 'jabber-muc-process-message)
+
+(defun jabber-muc-process-message (jc xml-data)
+ "If XML-DATA is a groupchat message, handle it as such."
+ (when (jabber-muc-message-p xml-data)
+ (let* ((from (jabber-xml-get-attribute xml-data 'from))
+ (group (jabber-jid-user from))
+ (nick (jabber-jid-resource from))
+ (error-p (jabber-xml-get-children xml-data 'error))
+ (type (cond
+ (error-p :muc-error)
+ ((string= nick (cdr (assoc group *jabber-active-groupchats*)))
+ :muc-local)
+ (t :muc-foreign)))
+ (body-text (car (jabber-xml-node-children
+ (car (jabber-xml-get-children
+ xml-data 'body)))))
+
+ (printers (append jabber-muc-printers jabber-chat-printers)))
+
+ (with-current-buffer (jabber-muc-create-buffer jc group)
+ (jabber-muc-snarf-topic xml-data)
+ ;; Call alert hooks only when something is output
+ (when (or error-p
+ (run-hook-with-args-until-success 'printers xml-data type :printp))
+ (jabber-maybe-print-rare-time
+ (ewoc-enter-last jabber-chat-ewoc (list type xml-data :time (current-time))))
+
+ ;; ...except if the message is part of history, in which
+ ;; case we don't want an alert.
+ (let ((children-namespaces (mapcar (lambda (x) (when (listp x) (jabber-xml-get-attribute x 'xmlns)))
+ (jabber-xml-node-children xml-data))))
+ (unless (or (member "urn:xmpp:delay" children-namespaces)
+ (member "jabber:x:delay" children-namespaces))
+ (dolist (hook '(jabber-muc-hooks jabber-alert-muc-hooks))
+ (run-hook-with-args hook
+ nick group (current-buffer) body-text
+ (funcall jabber-alert-muc-function
+ nick group (current-buffer) body-text))))))))))
+
+(defun jabber-muc-process-presence (jc presence)
+ (let* ((from (jabber-xml-get-attribute presence 'from))
+ (type (jabber-xml-get-attribute presence 'type))
+ (x-muc (find-if
+ (lambda (x) (equal (jabber-xml-get-attribute x 'xmlns)
+ "http://jabber.org/protocol/muc#user"))
+ (jabber-xml-get-children presence 'x)))
+ (group (jabber-jid-user from))
+ (nickname (jabber-jid-resource from))
+ (symbol (jabber-jid-symbol from))
+ (our-nickname (gethash symbol jabber-pending-groupchats))
+ (item (car (jabber-xml-get-children x-muc 'item)))
+ (actor (jabber-xml-get-attribute (car (jabber-xml-get-children item 'actor)) 'jid))
+ (reason (car (jabber-xml-node-children (car (jabber-xml-get-children item 'reason)))))
+ (error-node (car (jabber-xml-get-children presence 'error)))
+ (status-codes (if error-node
+ (list (jabber-xml-get-attribute error-node 'code))
+ (mapcar
+ (lambda (status-element)
+ (jabber-xml-get-attribute status-element 'code))
+ (jabber-xml-get-children x-muc 'status)))))
+ ;; handle leaving a room
+ (cond
+ ((or (string= type "unavailable") (string= type "error"))
+ ;; error from room itself? or are we leaving?
+ (if (or (null nickname)
+ (member "110" status-codes)
+ (string= nickname our-nickname))
+ ;; Assume that an error means that we were thrown out of the
+ ;; room...
+ (let* ((leavingp t)
+ (message (cond
+ ((string= type "error")
+ (cond
+ ;; ...except for certain cases.
+ ((or (member "406" status-codes)
+ (member "409" status-codes))
+ (setq leavingp nil)
+ (concat "Nickname change not allowed"
+ (when error-node
+ (concat ": " (jabber-parse-error error-node)))))
+ (t
+ (concat "Error entering room"
+ (when error-node
+ (concat ": " (jabber-parse-error error-node)))))))
+ ((member "301" status-codes)
+ (concat "You have been banned"
+ (when actor (concat " by " actor))
+ (when reason (concat " - '" reason "'"))))
+ ((member "307" status-codes)
+ (concat "You have been kicked"
+ (when actor (concat " by " actor))
+ (when reason (concat " - '" reason "'"))))
+ (t
+ "You have left the chatroom"))))
+ (when leavingp
+ (jabber-muc-remove-groupchat group))
+ ;; If there is no buffer for this groupchat, don't bother
+ ;; creating one just to tell that user left the room.
+ (let ((buffer (get-buffer (jabber-muc-get-buffer group))))
+ (if buffer
+ (with-current-buffer buffer
+ (jabber-maybe-print-rare-time
+ (ewoc-enter-last jabber-chat-ewoc
+ (list (if (string= type "error")
+ :muc-error
+ :muc-notice)
+ message
+ :time (current-time)))))
+ (message "%s: %s" (jabber-jid-displayname group) message))))
+ ;; or someone else?
+ (let* ((plist (jabber-muc-participant-plist group nickname))
+ (jid (plist-get plist 'jid))
+ (name (concat nickname
+ (when jid
+ (concat " <"
+ (jabber-jid-user jid)
+ ">")))))
+ (jabber-muc-remove-participant group nickname)
+ (with-current-buffer (jabber-muc-create-buffer jc group)
+ (jabber-maybe-print-rare-time
+ (ewoc-enter-last
+ jabber-chat-ewoc
+ (list :muc-notice
+ (cond
+ ((member "301" status-codes)
+ (concat name " has been banned"
+ (when actor (concat " by " actor))
+ (when reason (concat " - '" reason "'"))))
+ ((member "307" status-codes)
+ (concat name " has been kicked"
+ (when actor (concat " by " actor))
+ (when reason (concat " - '" reason "'"))))
+ ((member "303" status-codes)
+ (concat name " changes nickname to "
+ (jabber-xml-get-attribute item 'nick)))
+ (t
+ (concat name " has left the chatroom")))
+ :time (current-time))))))))
+ (t
+ ;; someone is entering
+
+ (when (or (member "110" status-codes) (string= nickname our-nickname))
+ ;; This is us. We just succeeded in entering the room.
+ ;;
+ ;; The MUC server is supposed to send a 110 code whenever this
+ ;; is our presence ("self-presence"), but at least one
+ ;; (ejabberd's mod_irc) doesn't, so check the nickname as well.
+ ;;
+ ;; This check might give incorrect results if the server
+ ;; changed our nickname to avoid collision with an existing
+ ;; participant, but even in this case the window where we have
+ ;; incorrect information should be very small, as we should be
+ ;; getting our own 110+210 presence shortly.
+ (let ((whichgroup (assoc group *jabber-active-groupchats*)))
+ (if whichgroup
+ (setcdr whichgroup nickname)
+ (add-to-list '*jabber-active-groupchats* (cons group nickname))))
+ ;; The server may have changed our nick. Record the new one.
+ (puthash symbol nickname jabber-pending-groupchats))
+
+ ;; Whoever enters, we create a buffer (if it didn't already
+ ;; exist), and print a notice. This is where autojoined MUC
+ ;; rooms have buffers created for them. We also remember some
+ ;; metadata.
+ (let ((old-plist (jabber-muc-participant-plist group nickname))
+ (new-plist (jabber-muc-parse-affiliation x-muc)))
+ (jabber-muc-modify-participant group nickname new-plist)
+ (let ((report (jabber-muc-report-delta nickname old-plist new-plist
+ reason actor)))
+ (when report
+ (with-current-buffer (jabber-muc-create-buffer jc group)
+ (jabber-maybe-print-rare-time
+ (ewoc-enter-last
+ jabber-chat-ewoc
+ (list :muc-notice report
+ :time (current-time))))
+ ;; Did the server change our nick?
+ (when (member "210" status-codes)
+ (ewoc-enter-last
+ jabber-chat-ewoc
+ (list :muc-notice
+ (concat "Your nick was changed to " nickname " by the server")
+ :time (current-time))))
+ ;; Was this room just created? If so, it's a locked
+ ;; room. Notify the user.
+ (when (member "201" status-codes)
+ (ewoc-enter-last
+ jabber-chat-ewoc
+ (list :muc-notice
+ (with-temp-buffer
+ (insert "This room was just created, and is locked to other participants.\n"
+ "To unlock it, ")
+ (insert-text-button
+ "configure the room"
+ 'action (apply-partially 'call-interactively 'jabber-muc-get-config))
+ (insert " or ")
+ (insert-text-button
+ "accept the default configuration"
+ 'action (apply-partially 'call-interactively 'jabber-muc-instant-config))
+ (insert ".")
+ (buffer-string))
+ :time (current-time))))))))))))
+
+(provide 'jabber-muc)
+
+;;; arch-tag: 1ff7ab35-1717-46ae-b803-6f5b3fb2cd7d