]>
Commit | Line | Data |
---|---|---|
1 | ;; jabber-muc.el - advanced MUC functions -*- lexical-binding: t; -*- | |
2 | ||
3 | ;; Copyright (C) 2010 - Kirill A. Korinskiy - catap@catap.ru | |
4 | ;; Copyright (C) 2003, 2004, 2007, 2008, 2009, 2010 - Magnus Henoch - mange@freemail.hu | |
5 | ;; Copyright (C) 2002, 2003, 2004 - tom berger - object@intelectronica.net | |
6 | ||
7 | ;; This file is a part of jabber.el. | |
8 | ||
9 | ;; This program is free software; you can redistribute it and/or modify | |
10 | ;; it under the terms of the GNU General Public License as published by | |
11 | ;; the Free Software Foundation; either version 2 of the License, or | |
12 | ;; (at your option) any later version. | |
13 | ||
14 | ;; This program is distributed in the hope that it will be useful, | |
15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
17 | ;; GNU General Public License for more details. | |
18 | ||
19 | ;; You should have received a copy of the GNU General Public License | |
20 | ;; along with this program; if not, write to the Free Software | |
21 | ;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
22 | ||
23 | (require 'jabber-chat) | |
24 | (require 'jabber-widget) | |
25 | (require 'jabber-disco) | |
26 | (require 'jabber-muc-nick-coloring) | |
27 | ||
28 | ;; we need jabber-bookmarks for jabber-muc-autojoin (via | |
29 | ;; jabber-get-bookmarks and jabber-parse-conference-bookmark): | |
30 | (require 'jabber-bookmarks) | |
31 | ||
32 | (require 'cl-lib) | |
33 | ||
34 | ;;;###autoload | |
35 | (defvar *jabber-active-groupchats* nil | |
36 | "Alist of groupchats and nicknames. | |
37 | Keys are strings, the bare JID of the room. | |
38 | Values are strings.") | |
39 | ||
40 | (defvar jabber-pending-groupchats (make-hash-table) | |
41 | "Hash table of groupchats and nicknames. | |
42 | Keys are JID symbols; values are strings. | |
43 | This table records the last nickname used to join the particular | |
44 | chat room. Items are thus never removed.") | |
45 | ||
46 | (defvar jabber-muc-participants nil | |
47 | "Alist of groupchats and participants. | |
48 | Keys are strings, the bare JID of the room. | |
49 | Values are lists of nickname strings.") | |
50 | ||
51 | (defvar jabber-group nil | |
52 | "The groupchat you are participating in.") | |
53 | ||
54 | (defvar jabber-muc-topic "" | |
55 | "The topic of the current MUC room.") | |
56 | ||
57 | (defvar jabber-role-history () | |
58 | "Keeps track of previously used roles.") | |
59 | ||
60 | (defvar jabber-affiliation-history () | |
61 | "Keeps track of previously used affiliations.") | |
62 | ||
63 | (defvar jabber-muc-nickname-history () | |
64 | "Keeps track of previously referred-to nicknames.") | |
65 | ||
66 | (defcustom jabber-muc-default-nicknames nil | |
67 | "Default nickname for specific MUC rooms." | |
68 | :group 'jabber-chat | |
69 | :type '(repeat | |
70 | (cons :format "%v" | |
71 | (string :tag "JID of room") | |
72 | (string :tag "Nickname")))) | |
73 | ||
74 | (defcustom jabber-muc-autojoin nil | |
75 | "List of MUC rooms to automatically join on connection. | |
76 | This list is saved in your Emacs customizations. You can also store | |
77 | such a list on the Jabber server, where it is available to every | |
78 | client; see `jabber-edit-bookmarks'." | |
79 | :group 'jabber-chat | |
80 | :type '(repeat (string :tag "JID of room"))) | |
81 | ||
82 | (defcustom jabber-muc-disable-disco-check nil | |
83 | "If non-nil, disable checking disco#info of rooms before joining them. | |
84 | Disco information can tell whether the room exists and whether it is | |
85 | password protected, but some servers do not support it. If you want | |
86 | to join chat rooms on such servers, set this variable to t." | |
87 | :group 'jabber-chat | |
88 | :type 'boolean) | |
89 | ||
90 | (defcustom jabber-groupchat-buffer-format "*-jabber-groupchat-%n-*" | |
91 | "The format specification for the name of groupchat buffers. | |
92 | ||
93 | These fields are available (all are about the group you are chatting | |
94 | in): | |
95 | ||
96 | %n Roster name of group, or JID if no nickname set | |
97 | %b Name of group from bookmarks or roster name or JID if none set | |
98 | %j Bare JID (without resource)" | |
99 | :type 'string | |
100 | :group 'jabber-chat) | |
101 | ||
102 | (defcustom jabber-groupchat-prompt-format "[%t] %n> " | |
103 | "The format specification for lines in groupchat. | |
104 | ||
105 | These fields are available: | |
106 | ||
107 | %t Time, formatted according to `jabber-chat-time-format' | |
108 | %n, %u, %r | |
109 | Nickname in groupchat | |
110 | %j Full JID (room@server/nick)" | |
111 | :type 'string | |
112 | :group 'jabber-chat) | |
113 | ||
114 | (defcustom jabber-muc-header-line-format | |
115 | '(" " (:eval (jabber-jid-displayname jabber-group)) | |
116 | "\t" jabber-muc-topic) | |
117 | "The specification for the header line of MUC buffers. | |
118 | ||
119 | The format is that of `mode-line-format' and `header-line-format'." | |
120 | :type 'sexp | |
121 | :group 'jabber-chat) | |
122 | ||
123 | (defcustom jabber-muc-private-buffer-format "*-jabber-muc-priv-%g-%n-*" | |
124 | "The format specification for the buffer name for private MUC messages. | |
125 | ||
126 | These fields are available: | |
127 | ||
128 | %g Roster name of group, or JID if no nickname set | |
129 | %n Nickname of the group member you're chatting with" | |
130 | :type 'string | |
131 | :group 'jabber-chat) | |
132 | ||
133 | (defcustom jabber-muc-private-foreign-prompt-format "[%t] %g/%n> " | |
134 | "The format specification for lines others type in a private MUC buffer. | |
135 | ||
136 | These fields are available: | |
137 | ||
138 | %t Time, formatted according to `jabber-chat-time-format' | |
139 | %n Nickname in room | |
140 | %g Short room name (either roster name or username part of JID)" | |
141 | :type 'string | |
142 | :group 'jabber-chat) | |
143 | ||
144 | (defcustom jabber-muc-print-names-format " %n %a %j\n" | |
145 | "The format specification for MUC list lines. | |
146 | ||
147 | Fields available: | |
148 | ||
149 | %n Nickname in room | |
150 | %a Affiliation status | |
151 | %j Full JID (room@server/nick)" | |
152 | :type 'string | |
153 | :group 'jabber-chat) | |
154 | ||
155 | (defcustom jabber-muc-private-header-line-format | |
156 | '(" " (:eval (jabber-jid-resource jabber-chatting-with)) | |
157 | " in " (:eval (jabber-jid-displayname (jabber-jid-user jabber-chatting-with))) | |
158 | "\t" jabber-events-message | |
159 | "\t" jabber-chatstates-message) | |
160 | "The specification for the header line of private MUC chat buffers. | |
161 | ||
162 | The format is that of `mode-line-format' and `header-line-format'." | |
163 | :type 'sexp | |
164 | :group 'jabber-chat) | |
165 | ||
166 | ;;;###autoload | |
167 | (defvar jabber-muc-printers '() | |
168 | "List of functions that may be able to print part of a MUC message. | |
169 | This gets prepended to `jabber-chat-printers', which see.") | |
170 | ||
171 | ;;;###autoload | |
172 | (defun jabber-muc-get-buffer (group) | |
173 | "Return the chat buffer for chatroom GROUP. | |
174 | Either a string or a buffer is returned, so use `get-buffer' or | |
175 | `get-buffer-create'." | |
176 | (format-spec jabber-groupchat-buffer-format | |
177 | (list | |
178 | (cons ?n (jabber-jid-displayname group)) | |
179 | (cons ?b (jabber-jid-bookmarkname group)) | |
180 | (cons ?j (jabber-jid-user group))))) | |
181 | ||
182 | (defun jabber-muc-create-buffer (jc group) | |
183 | "Prepare a buffer for chatroom GROUP. | |
184 | This function is idempotent. | |
185 | ||
186 | JC is the Jabber connection." | |
187 | (with-current-buffer (get-buffer-create (jabber-muc-get-buffer group)) | |
188 | (unless (eq major-mode 'jabber-chat-mode) | |
189 | (jabber-chat-mode jc #'jabber-chat-pp)) | |
190 | ;; Make sure the connection variable is up to date. | |
191 | (setq jabber-buffer-connection jc) | |
192 | ||
193 | (set (make-local-variable 'jabber-group) group) | |
194 | (make-local-variable 'jabber-muc-topic) | |
195 | (setq jabber-send-function 'jabber-muc-send) | |
196 | (setq header-line-format jabber-muc-header-line-format) | |
197 | (current-buffer))) | |
198 | ||
199 | ;;;###autoload | |
200 | (defun jabber-muc-private-get-buffer (group nickname) | |
201 | "Return the chat buffer for private chat with NICKNAME in GROUP. | |
202 | Either a string or a buffer is returned, so use `get-buffer' or | |
203 | `get-buffer-create'." | |
204 | (format-spec jabber-muc-private-buffer-format | |
205 | (list | |
206 | (cons ?g (jabber-jid-displayname group)) | |
207 | (cons ?n nickname)))) | |
208 | ||
209 | (defun jabber-muc-private-create-buffer (jc group nickname) | |
210 | "Prepare a buffer for chatting with NICKNAME in GROUP. | |
211 | This function is idempotent. | |
212 | ||
213 | JC is the Jabber connection." | |
214 | (with-current-buffer (get-buffer-create (jabber-muc-private-get-buffer group nickname)) | |
215 | (unless (eq major-mode 'jabber-chat-mode) | |
216 | (jabber-chat-mode jc #'jabber-chat-pp)) | |
217 | ||
218 | (set (make-local-variable 'jabber-chatting-with) (concat group "/" nickname)) | |
219 | (setq jabber-send-function 'jabber-chat-send) | |
220 | (setq header-line-format jabber-muc-private-header-line-format) | |
221 | ||
222 | (current-buffer))) | |
223 | ||
224 | (defun jabber-muc-send (jc body) | |
225 | "Send BODY to MUC room in current buffer. | |
226 | ||
227 | JC is the Jabber connection." | |
228 | ;; There is no need to display the sent message in the buffer, as | |
229 | ;; we will get it back from the MUC server. | |
230 | (jabber-send-sexp jc | |
231 | `(message | |
232 | ((to . ,jabber-group) | |
233 | (type . "groupchat")) | |
234 | (body () ,body)))) | |
235 | ||
236 | (defun jabber-muc-add-groupchat (group nickname) | |
237 | "Remember participating in GROUP under NICKNAME." | |
238 | (let ((whichgroup (assoc group *jabber-active-groupchats*))) | |
239 | (if whichgroup | |
240 | (setcdr whichgroup nickname) | |
241 | (add-to-list '*jabber-active-groupchats* (cons group nickname))))) | |
242 | ||
243 | (defun jabber-muc-remove-groupchat (group) | |
244 | "Remove GROUP from internal bookkeeping." | |
245 | (let ((whichgroup (assoc group *jabber-active-groupchats*)) | |
246 | (whichparticipants (assoc group jabber-muc-participants))) | |
247 | (setq *jabber-active-groupchats* | |
248 | (delq whichgroup *jabber-active-groupchats*)) | |
249 | (setq jabber-muc-participants | |
250 | (delq whichparticipants jabber-muc-participants)))) | |
251 | ||
252 | (defun jabber-muc-connection-closed (bare-jid) | |
253 | "Remove MUC data for BARE-JID. | |
254 | Forget all information about rooms that had been entered with | |
255 | this JID. Suitable to call when the connection is closed." | |
256 | (dolist (room-entry jabber-muc-participants) | |
257 | (let* ((room (car room-entry)) | |
258 | (buffer (get-buffer (jabber-muc-get-buffer room)))) | |
259 | (when (bufferp buffer) | |
260 | (with-current-buffer buffer | |
261 | (when (string= bare-jid | |
262 | (jabber-connection-bare-jid jabber-buffer-connection)) | |
263 | (setq *jabber-active-groupchats* | |
264 | (cl-delete room *jabber-active-groupchats* | |
265 | :key #'car :test #'string=)) | |
266 | (setq jabber-muc-participants | |
267 | (delq room-entry jabber-muc-participants)))))))) | |
268 | ||
269 | (defun jabber-muc-participant-plist (group nickname) | |
270 | "Return plist associated with NICKNAME in GROUP. | |
271 | Return nil if nothing known about that combination." | |
272 | (let ((whichparticipants (assoc group jabber-muc-participants))) | |
273 | (when whichparticipants | |
274 | (cdr (assoc nickname whichparticipants))))) | |
275 | ||
276 | (defun jabber-muc-modify-participant (group nickname new-plist) | |
277 | "Assign properties in NEW-PLIST to NICKNAME in GROUP." | |
278 | (let ((participants (assoc group jabber-muc-participants))) | |
279 | ;; either we have a list of participants already... | |
280 | (if participants | |
281 | (let ((participant (assoc nickname participants))) | |
282 | ;; and maybe this participant is already in the list | |
283 | (if participant | |
284 | ;; if so, just update role, affiliation, etc. | |
285 | (setf (cdr participant) new-plist) | |
286 | (push (cons nickname new-plist) (cdr participants)))) | |
287 | ;; or we don't | |
288 | (push (cons group (list (cons nickname new-plist))) jabber-muc-participants)))) | |
289 | ||
290 | (defun jabber-muc-report-delta (nickname old-plist new-plist reason actor) | |
291 | "Compare OLD-PLIST and NEW-PLIST, and return a string explaining the change. | |
292 | Return nil if nothing noteworthy has happened. | |
293 | NICKNAME is the user experiencing the change. REASON and ACTOR, if non-nil, | |
294 | are the corresponding presence fields. | |
295 | ||
296 | This function is only concerned with presence stanzas resulting | |
297 | in the user entering/staying in the room." | |
298 | ;; The keys in the plist are affiliation, role and jid. | |
299 | (when (plist-get new-plist 'jid) | |
300 | ;; nickname is only used for displaying, so we can modify it if we | |
301 | ;; want to. | |
302 | (setq nickname (concat nickname " <" | |
303 | (jabber-jid-user (plist-get new-plist 'jid)) | |
304 | ">"))) | |
305 | (cond | |
306 | ((null old-plist) | |
307 | ;; User enters the room | |
308 | (concat nickname " enters the room (" | |
309 | (plist-get new-plist 'role) | |
310 | (unless (string= (plist-get new-plist 'affiliation) "none") | |
311 | (concat ", " (plist-get new-plist 'affiliation))) | |
312 | ")")) | |
313 | ||
314 | ;; If affiliation changes, the role change is usually the logical | |
315 | ;; one, so don't report it separately. | |
316 | ((not (string= (plist-get old-plist 'affiliation) | |
317 | (plist-get new-plist 'affiliation))) | |
318 | (let ((actor-reason (concat (when actor | |
319 | (concat " by " actor)) | |
320 | (when reason | |
321 | (concat ": " reason)))) | |
322 | (from (plist-get old-plist 'affiliation)) | |
323 | (to (plist-get new-plist 'affiliation))) | |
324 | ;; There are many ways to express these transitions in English. | |
325 | ;; This one favors eloquence over regularity and consistency. | |
326 | (cond | |
327 | ;; Higher affiliation | |
328 | ((or (and (member from '("outcast" "none" "member")) | |
329 | (member to '("admin" "owner"))) | |
330 | (and (string= from "admin") (string= to "owner"))) | |
331 | (concat nickname " has been promoted to " to actor-reason)) | |
332 | ;; Lower affiliation | |
333 | ((or (and (member from '("owner" "admin")) | |
334 | (string= to "member")) | |
335 | (and (string= from "owner") (string= to "admin"))) | |
336 | (concat nickname " has been demoted to " to actor-reason)) | |
337 | ;; Become member | |
338 | ((string= to "member") | |
339 | (concat nickname " has been granted membership" actor-reason)) | |
340 | ;; Lose membership | |
341 | ((string= to "none") | |
342 | (concat nickname " has been deprived of membership" actor-reason))))) | |
343 | ||
344 | ;; Role changes | |
345 | ((not (string= (plist-get old-plist 'role) | |
346 | (plist-get new-plist 'role))) | |
347 | (let ((actor-reason (concat (when actor | |
348 | (concat " by " actor)) | |
349 | (when reason | |
350 | (concat ": " reason)))) | |
351 | (from (plist-get old-plist 'role)) | |
352 | (to (plist-get new-plist 'role))) | |
353 | ;; Possible roles are "none" (not in room, hence not of interest | |
354 | ;; in this function), "visitor" (no voice), "participant" (has | |
355 | ;; voice), and "moderator". | |
356 | (cond | |
357 | ((string= to "moderator") | |
358 | (concat nickname " has been granted moderator privileges" actor-reason)) | |
359 | ((and (string= from "moderator") | |
360 | (string= to "participant")) | |
361 | (concat nickname " had moderator privileges revoked" actor-reason)) | |
362 | ((string= to "participant") | |
363 | (concat nickname " has been granted voice" actor-reason)) | |
364 | ((string= to "visitor") | |
365 | (concat nickname " has been denied voice" actor-reason))))))) | |
366 | ||
367 | (defun jabber-muc-remove-participant (group nickname) | |
368 | "Forget everything about NICKNAME in GROUP." | |
369 | (let ((participants (assoc group jabber-muc-participants))) | |
370 | (when participants | |
371 | (let ((participant (assoc nickname (cdr participants)))) | |
372 | (setf (cdr participants) (delq participant (cdr participants))))))) | |
373 | ||
374 | (defmacro jabber-muc-argument-list (&optional args) | |
375 | "Prepend connection and group name to ARGS. | |
376 | If the current buffer is not an MUC buffer, signal an error. | |
377 | This macro is meant for use as an argument to `interactive'." | |
378 | `(if (null jabber-group) | |
379 | (error "Not in MUC buffer") | |
380 | (nconc (list jabber-buffer-connection jabber-group) ,args))) | |
381 | ||
382 | (defun jabber-muc-read-completing (prompt &optional allow-not-joined) | |
383 | "Read the name of a joined chatroom, or use chatroom of current buffer if any. | |
384 | If ALLOW-NOT-JOINED is provided and non-nil, permit choosing any | |
385 | JID; only provide completion as a guide." | |
386 | (or jabber-group | |
387 | (jabber-read-jid-completing prompt | |
388 | (if (null *jabber-active-groupchats*) | |
389 | (error "You haven't joined any group") | |
390 | (mapcar (lambda (x) (jabber-jid-symbol (car x))) | |
391 | *jabber-active-groupchats*)) | |
392 | (not allow-not-joined) | |
393 | jabber-group))) | |
394 | ||
395 | (defun jabber-muc-read-nickname (group prompt) | |
396 | "Read the nickname of a participant in GROUP." | |
397 | (let ((nicknames (cdr (assoc group jabber-muc-participants)))) | |
398 | (unless nicknames | |
399 | (error "Unknown group: %s" group)) | |
400 | (completing-read prompt nicknames nil t nil 'jabber-muc-nickname-history))) | |
401 | ||
402 | (add-to-list 'jabber-jid-muc-menu | |
403 | (cons "Request vcard" 'jabber-muc-vcard-get)) | |
404 | ||
405 | ;;;###autoload | |
406 | (defun jabber-muc-vcard-get (jc group nickname) | |
407 | "Request vcard from chat with NICKNAME in GROUP. | |
408 | ||
409 | JC is the Jabber connection." | |
410 | (interactive | |
411 | (jabber-muc-argument-list | |
412 | (list (jabber-muc-read-nickname jabber-group "Nickname: ")))) | |
413 | (let ((muc-name (format "%s/%s" group nickname))) | |
414 | (jabber-vcard-get jc muc-name))) | |
415 | ||
416 | (defun jabber-muc-instant-config (jc group) | |
417 | "Accept default configuration for GROUP. | |
418 | This can be used for a newly created room, as an alternative to | |
419 | filling out the configuration form with `jabber-muc-get-config'. | |
420 | Both of these methods unlock the room, so that other users can | |
421 | enter it. | |
422 | ||
423 | JC is the Jabber connection." | |
424 | (interactive (jabber-muc-argument-list)) | |
425 | (jabber-send-iq jc group | |
426 | "set" | |
427 | '(query ((xmlns . "http://jabber.org/protocol/muc#owner")) | |
428 | (x ((xmlns . "jabber:x:data") (type . "submit")))) | |
429 | #'jabber-report-success "MUC instant configuration" | |
430 | #'jabber-report-success "MUC instant configuration")) | |
431 | ||
432 | (add-to-list 'jabber-jid-muc-menu | |
433 | (cons "Configure groupchat" 'jabber-muc-get-config)) | |
434 | ||
435 | (defun jabber-muc-get-config (jc group) | |
436 | "Ask for MUC configuration form. | |
437 | ||
438 | JC is the Jabber connection." | |
439 | (interactive (jabber-muc-argument-list)) | |
440 | (jabber-send-iq jc group | |
441 | "get" | |
442 | '(query ((xmlns . "http://jabber.org/protocol/muc#owner"))) | |
443 | #'jabber-process-data #'jabber-muc-render-config | |
444 | #'jabber-process-data "MUC configuration request failed")) | |
445 | ||
446 | (defalias 'jabber-groupchat-get-config 'jabber-muc-get-config | |
447 | "Deprecated. See `jabber-muc-get-config' instead.") | |
448 | ||
449 | (defun jabber-muc-render-config (jc xml-data) | |
450 | "Render MUC configuration form. | |
451 | ||
452 | JC is the Jabber connection. | |
453 | XML-DATA is the parsed tree data from the stream (stanzas) | |
454 | obtained from `xml-parse-region'." | |
455 | ||
456 | (let ((query (jabber-iq-query xml-data)) | |
457 | xdata) | |
458 | (dolist (x (jabber-xml-get-children query 'x)) | |
459 | (if (string= (jabber-xml-get-attribute x 'xmlns) "jabber:x:data") | |
460 | (setq xdata x))) | |
461 | (if (not xdata) | |
462 | (insert "No configuration possible.\n") | |
463 | ||
464 | (jabber-init-widget-buffer (jabber-xml-get-attribute xml-data 'from)) | |
465 | (setq jabber-buffer-connection jc) | |
466 | ||
467 | (jabber-render-xdata-form xdata) | |
468 | ||
469 | (widget-create 'push-button :notify #'jabber-muc-submit-config "Submit") | |
470 | (widget-insert "\t") | |
471 | (widget-create 'push-button :notify #'jabber-muc-cancel-config "Cancel") | |
472 | (widget-insert "\n") | |
473 | ||
474 | (widget-setup) | |
475 | (widget-minor-mode 1)))) | |
476 | ||
477 | (defalias 'jabber-groupchat-render-config 'jabber-muc-render-config | |
478 | "Deprecated. See `jabber-muc-render-config' instead.") | |
479 | ||
480 | (defun jabber-muc-submit-config (&rest ignore) | |
481 | "Submit MUC configuration form." | |
482 | ||
483 | (jabber-send-iq jabber-buffer-connection jabber-submit-to | |
484 | "set" | |
485 | `(query ((xmlns . "http://jabber.org/protocol/muc#owner")) | |
486 | ,(jabber-parse-xdata-form)) | |
487 | #'jabber-report-success "MUC configuration" | |
488 | #'jabber-report-success "MUC configuration")) | |
489 | ||
490 | (defalias 'jabber-groupchat-submit-config 'jabber-muc-submit-config | |
491 | "Deprecated. See `jabber-muc-submit-config' instead.") | |
492 | ||
493 | (defun jabber-muc-cancel-config (&rest ignore) | |
494 | "Cancel MUC configuration form." | |
495 | ||
496 | (jabber-send-iq jabber-buffer-connection jabber-submit-to | |
497 | "set" | |
498 | '(query ((xmlns . "http://jabber.org/protocol/muc#owner")) | |
499 | (x ((xmlns . "jabber:x:data") (type . "cancel")))) | |
500 | nil nil nil nil)) | |
501 | ||
502 | (defalias 'jabber-groupchat-cancel-config 'jabber-muc-cancel-config | |
503 | "Deprecated. See `jabber-muc-cancel-config' instead.") | |
504 | ||
505 | (add-to-list 'jabber-jid-muc-menu | |
506 | (cons "Join groupchat" 'jabber-muc-join)) | |
507 | ||
508 | (defun jabber-muc-join (jc group nickname &optional popup) | |
509 | "Join a groupchat, or change nick. | |
510 | In interactive calls, or if POPUP is non-nil, switch to the | |
511 | groupchat buffer. | |
512 | ||
513 | JC is the Jabber connection." | |
514 | (interactive | |
515 | (let ((account (jabber-read-account)) | |
516 | (group (jabber-read-jid-completing "group: "))) | |
517 | (list account group (jabber-muc-read-my-nickname account group) t))) | |
518 | ||
519 | ;; If the user is already in the room, we don't need as many checks. | |
520 | (if (or (assoc group *jabber-active-groupchats*) | |
521 | ;; Or if the users asked us not to check disco info. | |
522 | jabber-muc-disable-disco-check) | |
523 | (jabber-muc-join-3 jc group nickname nil popup) | |
524 | ;; Else, send a disco request to find out what we are connecting | |
525 | ;; to. | |
526 | (jabber-disco-get-info jc group nil #'jabber-muc-join-2 | |
527 | (list group nickname popup)))) | |
528 | ||
529 | (defalias 'jabber-groupchat-join 'jabber-muc-join | |
530 | "Deprecated. Use `jabber-muc-join' instead.") | |
531 | ||
532 | (defun jabber-muc-join-2 (jc closure result) | |
533 | (cl-destructuring-bind (group nickname popup) closure | |
534 | (let* ( ;; Either success... | |
535 | (identities (car result)) | |
536 | (features (cadr result)) | |
537 | ;; ...or error | |
538 | (condition (when (eq identities 'error) (jabber-error-condition result)))) | |
539 | (cond | |
540 | ;; Maybe the room doesn't exist yet. | |
541 | ((eq condition 'item-not-found) | |
542 | (unless (or jabber-silent-mode | |
543 | (y-or-n-p (format "%s doesn't exist. Create it? " | |
544 | (jabber-jid-displayname group)))) | |
545 | (error "Non-existent groupchat"))) | |
546 | ||
547 | ;; Maybe the room doesn't support disco. | |
548 | ((eq condition 'feature-not-implemented) | |
549 | t ;whatever... we will ignore it later | |
550 | ) | |
551 | ;; Maybe another error occurred. Report it to user | |
552 | (condition | |
553 | (message "Couldn't query groupchat: %s" (jabber-parse-error result))) | |
554 | ||
555 | ;; Bad stanza? Without NS, for example | |
556 | ((and (eq identities 'error) (not condition)) | |
557 | (message "Bad error stanza received"))) | |
558 | ||
559 | ;; Continue only if it is really chat room. If there was an | |
560 | ;; error, give the chat room the benefit of the doubt. (Needed | |
561 | ;; for ejabberd's mod_irc, for example) | |
562 | (when (or condition | |
563 | (cl-find "conference" (if (sequencep identities) identities nil) | |
564 | :key (lambda (i) (aref i 1)) | |
565 | :test #'string=)) | |
566 | (let ((password | |
567 | ;; Is the room password-protected? | |
568 | (when (member "muc_passwordprotected" features) | |
569 | (or | |
570 | (jabber-get-conference-data jc group nil :password) | |
571 | (read-passwd (format "Password for %s: " (jabber-jid-displayname group))))))) | |
572 | ||
573 | (jabber-muc-join-3 jc group nickname password popup)))))) | |
574 | ||
575 | (defalias 'jabber-groupchat-join-2 'jabber-muc-join-2 | |
576 | "Deprecated. See `jabber-muc-join-2' instead.") | |
577 | ||
578 | (defun jabber-muc-join-3 (jc group nickname password popup) | |
579 | ||
580 | ;; Remember that this is a groupchat _before_ sending the stanza. | |
581 | ;; The response might come quicker than you think. | |
582 | ||
583 | (puthash (jabber-jid-symbol group) nickname jabber-pending-groupchats) | |
584 | ||
585 | (jabber-send-sexp jc | |
586 | `(presence ((to . ,(format "%s/%s" group nickname))) | |
587 | (x ((xmlns . "http://jabber.org/protocol/muc")) | |
588 | ,@(when password | |
589 | `((password () ,password)))) | |
590 | ,@(jabber-presence-children jc))) | |
591 | ||
592 | ;; There, stanza sent. Now we just wait for the MUC service to | |
593 | ;; mirror the stanza. This is handled in | |
594 | ;; `jabber-muc-process-presence', where a buffer will be created for | |
595 | ;; the room. | |
596 | ||
597 | ;; But if the user interactively asked to join, he/she probably | |
598 | ;; wants the buffer to pop up right now. | |
599 | (when popup | |
600 | (let ((buffer (jabber-muc-create-buffer jc group))) | |
601 | (switch-to-buffer buffer)))) | |
602 | ||
603 | (defalias 'jabber-groupchat-join-3 'jabber-muc-join-3 | |
604 | "Deprecated. See `jabber-muc-join-3' instead.") | |
605 | ||
606 | (defun jabber-muc-read-my-nickname (jc group &optional default) | |
607 | "Read nickname for joining GROUP. | |
608 | If DEFAULT is non-nil, return default nick without prompting. | |
609 | ||
610 | JC is the Jabber connection." | |
611 | (let ((default-nickname (or | |
612 | (jabber-get-conference-data jc group nil :nick) | |
613 | (cdr (assoc group jabber-muc-default-nicknames)) | |
614 | (plist-get (fsm-get-state-data jc) :username)))) | |
615 | (if default | |
616 | default-nickname | |
617 | (jabber-read-with-input-method (format "Nickname: (default %s) " | |
618 | default-nickname) | |
619 | nil nil default-nickname)))) | |
620 | ||
621 | (add-to-list 'jabber-jid-muc-menu | |
622 | (cons "Change nickname" 'jabber-muc-nick)) | |
623 | ||
624 | (defalias 'jabber-muc-nick 'jabber-muc-join) | |
625 | ||
626 | (add-to-list 'jabber-jid-muc-menu | |
627 | (cons "Leave groupchat" 'jabber-muc-leave)) | |
628 | ||
629 | (defun jabber-muc-leave (jc group) | |
630 | "Leave a groupchat. | |
631 | ||
632 | JC is the Jabber connection." | |
633 | (interactive (jabber-muc-argument-list)) | |
634 | (let ((whichgroup (assoc group *jabber-active-groupchats*))) | |
635 | ;; send unavailable presence to our own nick in room | |
636 | (jabber-send-sexp jc | |
637 | `(presence ((to . ,(format "%s/%s" group (cdr whichgroup))) | |
638 | (type . "unavailable")))))) | |
639 | ||
640 | (defalias 'jabber-groupchat-leave 'jabber-muc-leave | |
641 | "Deprecated. Use `jabber-muc-leave' instead.") | |
642 | ||
643 | (add-to-list 'jabber-jid-muc-menu | |
644 | (cons "List participants" 'jabber-muc-names)) | |
645 | ||
646 | (defun jabber-muc-names () | |
647 | "Print names, affiliations, and roles of participants in current buffer." | |
648 | (interactive) | |
649 | (ewoc-enter-last jabber-chat-ewoc (list :notice | |
650 | (jabber-muc-print-names | |
651 | (cdr (assoc jabber-group jabber-muc-participants))) | |
652 | :time (current-time)))) | |
653 | ||
654 | (defun jabber-muc-format-names (participant) | |
655 | "Format one participant name." | |
656 | (format-spec jabber-muc-print-names-format | |
657 | (list | |
658 | (cons ?n (car participant)) | |
659 | (cons ?a (plist-get (cdr participant) 'affiliation)) | |
660 | (cons ?j (or (plist-get (cdr participant) 'jid) ""))))) | |
661 | ||
662 | (defun jabber-muc-print-names (participants) | |
663 | "Format and return data in PARTICIPANTS." | |
664 | (let ((mlist) (plist) (vlist) (nlist)) | |
665 | (mapcar (lambda (x) | |
666 | (let ((role (plist-get (cdr x) 'role))) | |
667 | (cond ((string= role "moderator") | |
668 | (add-to-list 'mlist x)) | |
669 | ((string= role "participant") | |
670 | (add-to-list 'plist x)) | |
671 | ((string= role "visitor") | |
672 | (add-to-list 'vlist x)) | |
673 | ((string= role "none") | |
674 | (add-to-list 'nlist x))))) | |
675 | participants) | |
676 | (concat | |
677 | (apply 'concat "\nModerators:\n" (mapcar 'jabber-muc-format-names mlist)) | |
678 | (apply 'concat "\nParticipants:\n" (mapcar 'jabber-muc-format-names plist)) | |
679 | (apply 'concat "\nVisitors:\n" (mapcar 'jabber-muc-format-names vlist)) | |
680 | (apply 'concat "\nNones:\n" (mapcar 'jabber-muc-format-names nlist))))) | |
681 | ||
682 | (add-to-list 'jabber-jid-muc-menu | |
683 | (cons "Set topic" 'jabber-muc-set-topic)) | |
684 | ||
685 | (defun jabber-muc-set-topic (jc group topic) | |
686 | "Set topic of GROUP to TOPIC. | |
687 | ||
688 | JC is the Jabber connection." | |
689 | (interactive | |
690 | (jabber-muc-argument-list | |
691 | (list (jabber-read-with-input-method "New topic: " jabber-muc-topic)))) | |
692 | (jabber-send-message jc group topic nil "groupchat")) | |
693 | ||
694 | (defun jabber-muc-snarf-topic (xml-data) | |
695 | "Record subject (topic) of the given <message/>, if any. | |
696 | ||
697 | XML-DATA is the parsed tree data from the stream (stanzas) | |
698 | obtained from `xml-parse-region'." | |
699 | (let ((new-topic (jabber-xml-path xml-data '(subject "")))) | |
700 | (when new-topic | |
701 | (setq jabber-muc-topic new-topic)))) | |
702 | ||
703 | (add-to-list 'jabber-jid-muc-menu | |
704 | (cons "Set role (kick, voice, op)" 'jabber-muc-set-role)) | |
705 | ||
706 | (defun jabber-muc-set-role (jc group nickname role reason) | |
707 | "Set role of NICKNAME in GROUP to ROLE, specifying REASON. | |
708 | ||
709 | JC is the Jabber connection." | |
710 | (interactive | |
711 | (jabber-muc-argument-list | |
712 | (let ((nickname (jabber-muc-read-nickname jabber-group "Nickname: "))) | |
713 | (list nickname | |
714 | (completing-read "New role: " '(("none") ("visitor") ("participant") ("moderator")) nil t nil 'jabber-role-history) | |
715 | (read-string "Reason: "))))) | |
716 | (unless (or (zerop (length nickname)) (zerop (length role))) | |
717 | (jabber-send-iq jc group "set" | |
718 | `(query ((xmlns . "http://jabber.org/protocol/muc#admin")) | |
719 | (item ((nick . ,nickname) | |
720 | (role . ,role)) | |
721 | ,(unless (zerop (length reason)) | |
722 | `(reason () ,reason)))) | |
723 | 'jabber-report-success "Role change" | |
724 | 'jabber-report-success "Role change"))) | |
725 | ||
726 | (add-to-list 'jabber-jid-muc-menu | |
727 | (cons "Set affiliation (ban, member, admin)" 'jabber-muc-set-affiliation)) | |
728 | ||
729 | (defun jabber-muc-set-affiliation (jc group nickname-or-jid nickname-p affiliation reason) | |
730 | "Set affiliation of NICKNAME-OR-JID in GROUP to AFFILIATION. | |
731 | If NICKNAME-P is non-nil, NICKNAME-OR-JID is a nickname in the | |
732 | group, else it is a JID. | |
733 | ||
734 | JC is the Jabber connection." | |
735 | (interactive | |
736 | (jabber-muc-argument-list | |
737 | (let ((nickname-p (y-or-n-p "Specify user by room nickname? "))) | |
738 | (list | |
739 | (if nickname-p | |
740 | (jabber-muc-read-nickname jabber-group "Nickname: ") | |
741 | (jabber-read-jid-completing "User: ")) | |
742 | nickname-p | |
743 | (completing-read "New affiliation: " | |
744 | '(("none") ("outcast") ("member") ("admin") ("owner")) nil t nil 'jabber-affiliation-history) | |
745 | (read-string "Reason: "))))) | |
746 | (let ((jid | |
747 | (if nickname-p | |
748 | (let ((participants (cdr (assoc group jabber-muc-participants)))) | |
749 | (unless participants | |
750 | (error "Couldn't find group %s" group)) | |
751 | (let ((participant (cdr (assoc nickname-or-jid participants)))) | |
752 | (unless participant | |
753 | (error "Couldn't find %s in group %s" nickname-or-jid group)) | |
754 | (or (plist-get participant 'jid) | |
755 | (error "JID of %s in group %s is unknown" nickname-or-jid group)))) | |
756 | nickname-or-jid))) | |
757 | (jabber-send-iq jc group "set" | |
758 | `(query ((xmlns . "http://jabber.org/protocol/muc#admin")) | |
759 | (item ((jid . ,jid) | |
760 | (affiliation . ,affiliation)) | |
761 | ,(unless (zerop (length reason)) | |
762 | `(reason () ,reason)))) | |
763 | 'jabber-report-success "Affiliation change" | |
764 | 'jabber-report-success "Affiliation change"))) | |
765 | ||
766 | (add-to-list 'jabber-jid-muc-menu | |
767 | (cons "Invite someone to chatroom" 'jabber-muc-invite)) | |
768 | ||
769 | (defun jabber-muc-invite (jc jid group reason) | |
770 | "Invite JID to GROUP, stating REASON. | |
771 | ||
772 | JC is the Jabber connection." | |
773 | (interactive | |
774 | (list (jabber-read-account) | |
775 | (jabber-read-jid-completing | |
776 | "Invite whom: " | |
777 | ;; The current room is _not_ a good default for whom to invite. | |
778 | (remq (jabber-jid-symbol jabber-group) (jabber-concat-rosters))) | |
779 | (jabber-muc-read-completing "To group: ") | |
780 | (jabber-read-with-input-method "Reason: "))) | |
781 | (jabber-send-sexp | |
782 | jc | |
783 | `(message ((to . ,group)) | |
784 | (x ((xmlns . "http://jabber.org/protocol/muc#user")) | |
785 | (invite ((to . ,jid)) | |
786 | ,(unless (zerop (length reason)) | |
787 | `(reason nil ,reason))))))) | |
788 | ||
789 | (add-to-list 'jabber-body-printers 'jabber-muc-print-invite) | |
790 | ||
791 | (defun jabber-muc-print-invite (xml-data who mode) | |
792 | "Print MUC invitation. | |
793 | ||
794 | XML-DATA is the parsed tree data from the stream (stanzas) | |
795 | obtained from `xml-parse-region'." | |
796 | (cl-dolist (x (jabber-xml-get-children xml-data 'x)) | |
797 | (when (string= (jabber-xml-get-attribute x 'xmlns) "http://jabber.org/protocol/muc#user") | |
798 | (let ((invitation (car (jabber-xml-get-children x 'invite)))) | |
799 | (when invitation | |
800 | (when (eql mode :insert) | |
801 | (let ((group (jabber-xml-get-attribute xml-data 'from)) | |
802 | (inviter (jabber-xml-get-attribute invitation 'from)) | |
803 | (reason (car (jabber-xml-node-children (car (jabber-xml-get-children invitation 'reason)))))) | |
804 | ;; XXX: password | |
805 | (insert "You have been invited to MUC room " (jabber-jid-displayname group)) | |
806 | (when inviter | |
807 | (insert " by " (jabber-jid-displayname inviter))) | |
808 | (insert ".") | |
809 | (when reason | |
810 | (insert " Reason: " reason)) | |
811 | (insert "\n\n") | |
812 | ||
813 | (let ((action | |
814 | `(lambda (&rest ignore) (interactive) | |
815 | (jabber-muc-join jabber-buffer-connection ,group | |
816 | (jabber-muc-read-my-nickname jabber-buffer-connection ,group))))) | |
817 | (if (fboundp 'insert-button) | |
818 | (insert-button "Accept" | |
819 | 'action action) | |
820 | ;; Simple button replacement | |
821 | (let ((keymap (make-keymap))) | |
822 | (define-key keymap "\r" action) | |
823 | (insert (jabber-propertize "Accept" | |
824 | 'keymap keymap | |
825 | 'face 'highlight)))) | |
826 | ||
827 | (insert "\t") | |
828 | ||
829 | (let ((action | |
830 | `(lambda (&rest ignore) (interactive) | |
831 | (let ((reason | |
832 | (jabber-read-with-input-method | |
833 | "Reason: "))) | |
834 | (jabber-send-sexp | |
835 | jabber-buffer-connection | |
836 | (list 'message | |
837 | (list (cons 'to ,group)) | |
838 | (list 'x | |
839 | (list (cons 'xmlns "http://jabber.org/protocol/muc#user")) | |
840 | (list 'decline | |
841 | (list (cons 'to ,inviter)) | |
842 | (unless (zerop (length reason)) | |
843 | (list 'reason nil reason)))))))))) | |
844 | (if (fboundp 'insert-button) | |
845 | (insert-button "Decline" | |
846 | 'action action) | |
847 | ;; Simple button replacement | |
848 | (let ((keymap (make-keymap))) | |
849 | (define-key keymap "\r" action) | |
850 | (insert (jabber-propertize "Decline" | |
851 | 'keymap keymap | |
852 | 'face 'highlight)))))))) | |
853 | (cl-return t)))))) | |
854 | ||
855 | (defun jabber-muc-autojoin (jc) | |
856 | "Join rooms specified in account bookmarks and global `jabber-muc-autojoin'. | |
857 | ||
858 | JC is the Jabber connection." | |
859 | (interactive (list (jabber-read-account))) | |
860 | (let ((nickname (plist-get (fsm-get-state-data jc) :username))) | |
861 | (when (bound-and-true-p jabber-muc-autojoin) | |
862 | (dolist (group jabber-muc-autojoin) | |
863 | (jabber-muc-join jc group (or | |
864 | (cdr (assoc group jabber-muc-default-nicknames)) | |
865 | (plist-get (fsm-get-state-data jc) :username))))) | |
866 | (jabber-get-bookmarks | |
867 | jc | |
868 | (lambda (jc bookmarks) | |
869 | (dolist (bookmark bookmarks) | |
870 | (setq bookmark (jabber-parse-conference-bookmark bookmark)) | |
871 | (when (and bookmark (plist-get bookmark :autojoin)) | |
872 | (jabber-muc-join jc (plist-get bookmark :jid) | |
873 | (or (plist-get bookmark :nick) | |
874 | (plist-get (fsm-get-state-data jc) :username))))))))) | |
875 | ||
876 | ;;;###autoload | |
877 | (defun jabber-muc-message-p (message) | |
878 | "Return non-nil if MESSAGE is a groupchat message. | |
879 | That does not include private messages in a groupchat, but does | |
880 | include groupchat invites." | |
881 | ;; Public groupchat messages have type "groupchat" and are from | |
882 | ;; room@server/nick. Public groupchat errors have type "error" and | |
883 | ;; are from room@server. | |
884 | (let ((from (jabber-xml-get-attribute message 'from)) | |
885 | (type (jabber-xml-get-attribute message 'type))) | |
886 | (or | |
887 | (string= type "groupchat") | |
888 | (and (string= type "error") | |
889 | (gethash (jabber-jid-symbol from) jabber-pending-groupchats)) | |
890 | (jabber-xml-path message '(("http://jabber.org/protocol/muc#user" . "x") invite))))) | |
891 | ||
892 | ;;;###autoload | |
893 | (defun jabber-muc-sender-p (jid) | |
894 | "Return non-nil if JID is a full JID of an MUC participant." | |
895 | (and (assoc (jabber-jid-user jid) *jabber-active-groupchats*) | |
896 | (jabber-jid-resource jid))) | |
897 | ||
898 | ;;;###autoload | |
899 | (defun jabber-muc-private-message-p (message) | |
900 | "Return non-nil if MESSAGE is a private message in a groupchat." | |
901 | (let ((from (jabber-xml-get-attribute message 'from)) | |
902 | (type (jabber-xml-get-attribute message 'type))) | |
903 | (and | |
904 | (not (string= type "groupchat")) | |
905 | (jabber-muc-sender-p from)))) | |
906 | ||
907 | (add-to-list 'jabber-jid-muc-menu | |
908 | (cons "Open private chat" 'jabber-muc-private)) | |
909 | ||
910 | (defun jabber-muc-private (jc group nickname) | |
911 | "Open private chat with NICKNAME in GROUP. | |
912 | ||
913 | JC is the Jabber connection." | |
914 | (interactive | |
915 | (jabber-muc-argument-list | |
916 | (list (jabber-muc-read-nickname jabber-group "Nickname: ")))) | |
917 | (switch-to-buffer (jabber-muc-private-create-buffer jabber-buffer-connection group nickname))) | |
918 | ||
919 | (defun jabber-muc-presence-p (presence) | |
920 | "Return non-nil if PRESENCE is presence from groupchat." | |
921 | (let ((from (jabber-xml-get-attribute presence 'from)) | |
922 | (type (jabber-xml-get-attribute presence 'type)) | |
923 | (muc-marker (cl-find-if | |
924 | (lambda (x) (equal (jabber-xml-get-attribute x 'xmlns) | |
925 | "http://jabber.org/protocol/muc#user")) | |
926 | (jabber-xml-get-children presence 'x)))) | |
927 | ;; This is MUC presence if it has an MUC-namespaced tag... | |
928 | (or muc-marker | |
929 | ;; ...or if it is error presence from a room we tried to join. | |
930 | (and (string= type "error") | |
931 | (gethash (jabber-jid-symbol from) jabber-pending-groupchats))))) | |
932 | ||
933 | (defun jabber-muc-parse-affiliation (x-muc) | |
934 | "Parse X-MUC in the muc#user namespace and return a plist. | |
935 | Return nil if X-MUC is nil." | |
936 | ;; XXX: parse <actor/> and <reason/> tags? or maybe elsewhere? | |
937 | (apply 'nconc (mapcar (lambda (prop) (list (car prop) (cdr prop))) | |
938 | (jabber-xml-node-attributes | |
939 | (car (jabber-xml-get-children x-muc 'item)))))) | |
940 | ||
941 | (defun jabber-muc-print-prompt (xml-data &optional local dont-print-nick-p) | |
942 | "Print MUC prompt for message in XML-DATA." | |
943 | (let ((nick (jabber-jid-resource (jabber-xml-get-attribute xml-data 'from))) | |
944 | (timestamp (jabber-message-timestamp xml-data))) | |
945 | (if (stringp nick) | |
946 | (insert (jabber-propertize | |
947 | (format-spec jabber-groupchat-prompt-format | |
948 | (list | |
949 | (cons ?t (format-time-string | |
950 | (if timestamp | |
951 | jabber-chat-delayed-time-format | |
952 | jabber-chat-time-format) | |
953 | timestamp)) | |
954 | (cons ?n (if dont-print-nick-p "" nick)) | |
955 | (cons ?u nick) | |
956 | (cons ?r nick) | |
957 | (cons ?j (concat jabber-group "/" nick)))) | |
958 | 'face (if local ;Message from you. | |
959 | (if jabber-muc-colorize-local ;; If colorization enable... | |
960 | ;; ...colorize nick | |
961 | (list ':foreground (jabber-muc-nick-get-color nick)) | |
962 | ;; otherwise, use default face. | |
963 | 'jabber-chat-prompt-local) | |
964 | ;; Message from other participant. | |
965 | (if jabber-muc-colorize-foreign ;If colorization enable... | |
966 | ;; ... colorize nick | |
967 | (list ':foreground (jabber-muc-nick-get-color nick)) | |
968 | ;; otherwise, use default face. | |
969 | 'jabber-chat-prompt-foreign)) | |
970 | 'help-echo (concat (format-time-string "On %Y-%m-%d %H:%M:%S" timestamp) " from " nick " in " jabber-group))) | |
971 | (jabber-muc-system-prompt)))) | |
972 | ||
973 | (defun jabber-muc-private-print-prompt (xml-data) | |
974 | "Print prompt for private MUC message in XML-DATA." | |
975 | (let ((nick (jabber-jid-resource (jabber-xml-get-attribute xml-data 'from))) | |
976 | (group (jabber-jid-user (jabber-xml-get-attribute xml-data 'from))) | |
977 | (timestamp (jabber-message-timestamp xml-data))) | |
978 | (insert (jabber-propertize | |
979 | (format-spec jabber-muc-private-foreign-prompt-format | |
980 | (list | |
981 | (cons ?t (format-time-string | |
982 | (if timestamp | |
983 | jabber-chat-delayed-time-format | |
984 | jabber-chat-time-format) | |
985 | timestamp)) | |
986 | (cons ?n nick) | |
987 | (cons ?g (or (jabber-jid-rostername group) | |
988 | (jabber-jid-username group))))) | |
989 | 'face 'jabber-chat-prompt-foreign | |
990 | 'help-echo (concat (format-time-string "On %Y-%m-%d %H:%M:%S" timestamp) " from " nick " in " jabber-group))))) | |
991 | ||
992 | (defun jabber-muc-system-prompt (&rest ignore) | |
993 | "Print system prompt for MUC." | |
994 | (insert (jabber-propertize | |
995 | (format-spec jabber-groupchat-prompt-format | |
996 | (list | |
997 | (cons ?t (format-time-string jabber-chat-time-format)) | |
998 | (cons ?n "") | |
999 | (cons ?u "") | |
1000 | (cons ?r "") | |
1001 | (cons ?j jabber-group))) | |
1002 | 'face 'jabber-chat-prompt-system | |
1003 | 'help-echo (format-time-string "System message on %Y-%m-%d %H:%M:%S")))) | |
1004 | ||
1005 | (add-to-list 'jabber-message-chain 'jabber-muc-process-message) | |
1006 | ||
1007 | (defun jabber-muc-process-message (jc xml-data) | |
1008 | "If XML-DATA is a groupchat message, handle it as such. | |
1009 | ||
1010 | JC is the Jabber connection." | |
1011 | (when (jabber-muc-message-p xml-data) | |
1012 | (defvar printers nil) | |
1013 | (let* ((from (jabber-xml-get-attribute xml-data 'from)) | |
1014 | (group (jabber-jid-user from)) | |
1015 | (nick (jabber-jid-resource from)) | |
1016 | (error-p (jabber-xml-get-children xml-data 'error)) | |
1017 | (type (cond | |
1018 | (error-p :muc-error) | |
1019 | ((string= nick (cdr (assoc group *jabber-active-groupchats*))) | |
1020 | :muc-local) | |
1021 | (t :muc-foreign))) | |
1022 | (body-text (car (jabber-xml-node-children | |
1023 | (car (jabber-xml-get-children | |
1024 | xml-data 'body))))) | |
1025 | ||
1026 | (printers (append jabber-muc-printers jabber-chat-printers))) | |
1027 | ||
1028 | (with-current-buffer (jabber-muc-create-buffer jc group) | |
1029 | (jabber-muc-snarf-topic xml-data) | |
1030 | ;; Call alert hooks only when something is output | |
1031 | (when (or error-p | |
1032 | (run-hook-with-args-until-success 'printers xml-data type :printp)) | |
1033 | (jabber-maybe-print-rare-time | |
1034 | (ewoc-enter-last jabber-chat-ewoc (list type xml-data :time (current-time)))) | |
1035 | ||
1036 | ;; ...except if the message is part of history, in which | |
1037 | ;; case we don't want an alert. | |
1038 | (let ((children-namespaces (mapcar (lambda (x) (when (listp x) (jabber-xml-get-attribute x 'xmlns))) | |
1039 | (jabber-xml-node-children xml-data)))) | |
1040 | (unless (or (member "urn:xmpp:delay" children-namespaces) | |
1041 | (member "jabber:x:delay" children-namespaces)) | |
1042 | (dolist (hook '(jabber-muc-hooks jabber-alert-muc-hooks)) | |
1043 | (run-hook-with-args hook | |
1044 | nick group (current-buffer) body-text | |
1045 | (funcall jabber-alert-muc-function | |
1046 | nick group (current-buffer) body-text)))))))))) | |
1047 | ||
1048 | (defface jabber-muc-presence-dim | |
1049 | '((t (:foreground "dark grey" :weight light :slant italic))) | |
1050 | "face for diminished presence notifications.") | |
1051 | ||
1052 | (defcustom jabber-muc-decorate-presence-patterns nil | |
1053 | "List of regular expressions and face pairs. | |
1054 | When a presence notification matches a pattern, display it with | |
1055 | associated face. Ignore notification if face is ‘nil’." | |
1056 | :type '(repeat | |
1057 | :tag "Patterns" | |
1058 | (cons :format "%v" | |
1059 | (regexp :tag "Regexp") | |
1060 | (choice | |
1061 | (const :tag "Ignore" nil) | |
1062 | (face :tag "Face" :value jabber-muc-presence-dim)))) | |
1063 | :group 'jabber-alerts) | |
1064 | ||
1065 | (defun jabber-muc-maybe-decorate-presence (node) | |
1066 | "Filter presence notifications." | |
1067 | (cl-destructuring-bind (key msg &key time) node | |
1068 | (let* ((match (cl-find-if | |
1069 | (lambda (pair) | |
1070 | (string-match (car pair) msg)) | |
1071 | jabber-muc-decorate-presence-patterns)) | |
1072 | (face (cdr-safe match))) | |
1073 | (if match | |
1074 | (when face | |
1075 | (jabber-maybe-print-rare-time | |
1076 | (ewoc-enter-last | |
1077 | jabber-chat-ewoc | |
1078 | (list key | |
1079 | (propertize msg 'face face) | |
1080 | :time time)))) | |
1081 | (jabber-maybe-print-rare-time | |
1082 | (ewoc-enter-last jabber-chat-ewoc node)))))) | |
1083 | ||
1084 | (defun jabber-muc-process-presence (jc presence) | |
1085 | (let* ((from (jabber-xml-get-attribute presence 'from)) | |
1086 | (type (jabber-xml-get-attribute presence 'type)) | |
1087 | (x-muc (cl-find-if | |
1088 | (lambda (x) (equal (jabber-xml-get-attribute x 'xmlns) | |
1089 | "http://jabber.org/protocol/muc#user")) | |
1090 | (jabber-xml-get-children presence 'x))) | |
1091 | (group (jabber-jid-user from)) | |
1092 | (nickname (jabber-jid-resource from)) | |
1093 | (symbol (jabber-jid-symbol from)) | |
1094 | (our-nickname (gethash symbol jabber-pending-groupchats)) | |
1095 | (item (car (jabber-xml-get-children x-muc 'item))) | |
1096 | (actor (jabber-xml-get-attribute (car (jabber-xml-get-children item 'actor)) 'jid)) | |
1097 | (reason (car (jabber-xml-node-children (car (jabber-xml-get-children item 'reason))))) | |
1098 | (error-node (car (jabber-xml-get-children presence 'error))) | |
1099 | (status-codes (if error-node | |
1100 | (list (jabber-xml-get-attribute error-node 'code)) | |
1101 | (mapcar | |
1102 | (lambda (status-element) | |
1103 | (jabber-xml-get-attribute status-element 'code)) | |
1104 | (jabber-xml-get-children x-muc 'status))))) | |
1105 | ;; handle leaving a room | |
1106 | (cond | |
1107 | ((or (string= type "unavailable") (string= type "error")) | |
1108 | ;; error from room itself? or are we leaving? | |
1109 | (if (or (null nickname) | |
1110 | (member "110" status-codes) | |
1111 | (string= nickname our-nickname)) | |
1112 | ;; Assume that an error means that we were thrown out of the | |
1113 | ;; room... | |
1114 | (let* ((leavingp t) | |
1115 | (message (cond | |
1116 | ((string= type "error") | |
1117 | (cond | |
1118 | ;; ...except for certain cases. | |
1119 | ((or (member "406" status-codes) | |
1120 | (member "409" status-codes)) | |
1121 | (setq leavingp nil) | |
1122 | (concat "Nickname change not allowed" | |
1123 | (when error-node | |
1124 | (concat ": " (jabber-parse-error error-node))))) | |
1125 | (t | |
1126 | (concat "Error entering room" | |
1127 | (when error-node | |
1128 | (concat ": " (jabber-parse-error error-node))))))) | |
1129 | ((member "301" status-codes) | |
1130 | (concat "You have been banned" | |
1131 | (when actor (concat " by " actor)) | |
1132 | (when reason (concat " - '" reason "'")))) | |
1133 | ((member "307" status-codes) | |
1134 | (concat "You have been kicked" | |
1135 | (when actor (concat " by " actor)) | |
1136 | (when reason (concat " - '" reason "'")))) | |
1137 | (t | |
1138 | "You have left the chatroom")))) | |
1139 | (when leavingp | |
1140 | (jabber-muc-remove-groupchat group)) | |
1141 | ;; If there is no buffer for this groupchat, don't bother | |
1142 | ;; creating one just to tell that user left the room. | |
1143 | (let ((buffer (get-buffer (jabber-muc-get-buffer group)))) | |
1144 | (if buffer | |
1145 | (with-current-buffer buffer | |
1146 | (jabber-muc-maybe-decorate-presence | |
1147 | (list (if (string= type "error") | |
1148 | :muc-error | |
1149 | :muc-notice) | |
1150 | message | |
1151 | :time (current-time)))) | |
1152 | (message "%s: %s" (jabber-jid-displayname group) message)))) | |
1153 | ;; or someone else? | |
1154 | (let* ((plist (jabber-muc-participant-plist group nickname)) | |
1155 | (jid (plist-get plist 'jid)) | |
1156 | (name (concat nickname | |
1157 | (when jid | |
1158 | (concat " <" | |
1159 | (jabber-jid-user jid) | |
1160 | ">"))))) | |
1161 | (jabber-muc-remove-participant group nickname) | |
1162 | (with-current-buffer (jabber-muc-create-buffer jc group) | |
1163 | (jabber-muc-maybe-decorate-presence | |
1164 | (list :muc-notice | |
1165 | (cond | |
1166 | ((member "301" status-codes) | |
1167 | (concat name " has been banned" | |
1168 | (when actor (concat " by " actor)) | |
1169 | (when reason (concat " - '" reason "'")))) | |
1170 | ((member "307" status-codes) | |
1171 | (concat name " has been kicked" | |
1172 | (when actor (concat " by " actor)) | |
1173 | (when reason (concat " - '" reason "'")))) | |
1174 | ((member "303" status-codes) | |
1175 | (concat name " changes nickname to " | |
1176 | (jabber-xml-get-attribute item 'nick))) | |
1177 | (t | |
1178 | (concat name " has left the chatroom"))) | |
1179 | :time (current-time))))))) | |
1180 | (t | |
1181 | ;; someone is entering | |
1182 | ||
1183 | (when (or (member "110" status-codes) (string= nickname our-nickname)) | |
1184 | ;; This is us. We just succeeded in entering the room. | |
1185 | ;; | |
1186 | ;; The MUC server is supposed to send a 110 code whenever this | |
1187 | ;; is our presence ("self-presence"), but at least one | |
1188 | ;; (ejabberd's mod_irc) doesn't, so check the nickname as well. | |
1189 | ;; | |
1190 | ;; This check might give incorrect results if the server | |
1191 | ;; changed our nickname to avoid collision with an existing | |
1192 | ;; participant, but even in this case the window where we have | |
1193 | ;; incorrect information should be very small, as we should be | |
1194 | ;; getting our own 110+210 presence shortly. | |
1195 | (let ((whichgroup (assoc group *jabber-active-groupchats*))) | |
1196 | (if whichgroup | |
1197 | (setcdr whichgroup nickname) | |
1198 | (add-to-list '*jabber-active-groupchats* (cons group nickname)))) | |
1199 | ;; The server may have changed our nick. Record the new one. | |
1200 | (puthash symbol nickname jabber-pending-groupchats)) | |
1201 | ||
1202 | ;; Whoever enters, we create a buffer (if it didn't already | |
1203 | ;; exist), and print a notice. This is where autojoined MUC | |
1204 | ;; rooms have buffers created for them. We also remember some | |
1205 | ;; metadata. | |
1206 | (let ((old-plist (jabber-muc-participant-plist group nickname)) | |
1207 | (new-plist (jabber-muc-parse-affiliation x-muc))) | |
1208 | (jabber-muc-modify-participant group nickname new-plist) | |
1209 | (let ((report (jabber-muc-report-delta nickname old-plist new-plist | |
1210 | reason actor))) | |
1211 | (when report | |
1212 | (with-current-buffer (jabber-muc-create-buffer jc group) | |
1213 | (jabber-muc-maybe-decorate-presence | |
1214 | (list :muc-notice report | |
1215 | :time (current-time))) | |
1216 | ;; Did the server change our nick? | |
1217 | (when (member "210" status-codes) | |
1218 | (ewoc-enter-last | |
1219 | jabber-chat-ewoc | |
1220 | (list :muc-notice | |
1221 | (concat "Your nick was changed to " nickname " by the server") | |
1222 | :time (current-time)))) | |
1223 | ;; Was this room just created? If so, it's a locked | |
1224 | ;; room. Notify the user. | |
1225 | (when (member "201" status-codes) | |
1226 | (ewoc-enter-last | |
1227 | jabber-chat-ewoc | |
1228 | (list :muc-notice | |
1229 | (with-temp-buffer | |
1230 | (insert "This room was just created, and is locked to other participants.\n" | |
1231 | "To unlock it, ") | |
1232 | (insert-text-button | |
1233 | "configure the room" | |
1234 | 'action (apply-partially 'call-interactively 'jabber-muc-get-config)) | |
1235 | (insert " or ") | |
1236 | (insert-text-button | |
1237 | "accept the default configuration" | |
1238 | 'action (apply-partially 'call-interactively 'jabber-muc-instant-config)) | |
1239 | (insert ".") | |
1240 | (buffer-string)) | |
1241 | :time (current-time)))))))))))) | |
1242 | ||
1243 | (provide 'jabber-muc) | |
1244 | ||
1245 | ;;; arch-tag: 1ff7ab35-1717-46ae-b803-6f5b3fb2cd7d |