;;; erc-members.el --- ERC member management;; Copyright (C) 2003 Free Software Foundation, Inc.;; Author: Andreas Fuchs <asf@void.at>, ;; Alex Schroeder <alex@gnu.org>;; This 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, or (at your option);; any later version.;; This 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 GNU Emacs; see the file COPYING. If not, write to;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,;; Boston, MA 02111-1307, USA.;;; Commentary:;; An implementation of management functions which act as a;; replacement of the erc-channel-member lists. Note that when lines;; are parsed from the server, the nick is not downcased using;; `erc-downcase'. Thus, when comparing nicks, we always have to call;; `erc-downcase'. `erc-members' is a hash of hashes, where the;; second hash uses the downcased version of nicks as the key.;; Whenever gethash or puthash is used on it, this has to be;; considered.;; Eventually, remove erc-update-channel-info-buffer!;;; Code:;; Avoid miscompiling macro `erc-log' and `with-erc-channel-buffer' in;; absence of loaded definition from 'erc.;; See 2004-03-15_23-01_macro_err_1.txt in;; http://labb.contactor.se/~matsl/smoketest/logs/;; or newer results for miscompiled macros.(eval-when-compile(require'erc))(require'cl); for defstruct(defvarerc-members(makehash'eq)"A hash containing all channel members.The key is the `erc-process', and the value is yet another hash. Thisother hash uses the erc-downcased nick name as a key, and returns anerc-person structure.")(defcustomerc-members-changed-hooknil"*This hook is called everytime the variable `erc-members' changes.Note that if a bunch of changes happen at the same time, the hook is onlycalled once, at the end.":group'erc-hooks:type'hook)(make-obsolete-variable'erc-channel-members-changed-hook'erc-members-changed-hook)(when(and(boundp'erc-channel-members-changed-hook)erc-channel-members-changed-hook)(nconcerc-members-changed-hookerc-channel-members-changed-hook));; These erc-person structures are created in erc-add-nick-to-channel(defstructerc-personnickuserhostfull-nameemailinfochannels);; When creating a new erc-person, make sure to use (makehash 'equal);; for CHANNELS!;; CHANNELS is a hashtable of channel names as key and a list of modes,;; starting with nil, as a value. Other members of this list are 'op;; and 'voice. That is, if a person is in channel foo, (gethash "foo";; CHANNELS) returns (nil), or (op) or (voice) or even (op voice).(defunerc-person-debug(person)"Return a human readable representation of PERSON.PERSON is an erc-person structure."(list(erc-person-nickperson)(erc-person-userperson)(erc-person-hostperson)(erc-person-full-nameperson)(let(result)(maphash(lambda(keyval)(if(equalval'(nil))(setqresult(conskeyresult))(setqresult(cons(conskeyval)result))))(erc-person-channelsperson))result)));(eval-when-compile; (let ((p (make-erc-person :nick "test" :channels (makehash 'equal)))); (erc-add-person-to-channel p "foo" '(ga)); (erc-person-debug p)))(defunerc-members-debug()"Return a human readable representation of `erc-members'."(let(result)(maphash(lambda(keyval)(setqresult(cons(conskey(let(l)(maphash(lambda(kv)(setql(cons(consk(list(erc-person-debugv)))l)))val)l))result)))erc-members)result));;; Accessor Functions.;; In many cases there are two accessors: One for persons -- these are;; internal functions, and one for nicks -- these are the functions to;; be used from the outside. The goal is to call `erc-person' only;; once per nick.(defunerc-members-reset()"Clear the `erc-members' hash-table."(setqerc-members(makehash'eq)))(defunerc-members(&optionalprocess)"Return the hash of nicks for PROCESS.If PROCESS is nil, return the nick names for `erc-process'."(gethash(orprocesserc-process)erc-members))(defunerc-person-in-channel(personchannel)"Return non-nil if PERSON is in CHANNEL.PERSON is an erc-person structure."(gethashchannel(erc-person-channelsperson)))(defunerc-person(nick&optionalprocess)"Return the erc-person structure for NICK and PROCESS.If PROCESS is nil, use `erc-process'."(gethash(erc-downcasenick)(erc-membersprocess)))(defunerc-nick-in-channel(nickchannel&optionalprocess)"Return non-nil if NICK is in CHANNEL for PROCESS.If PROCESS is nil, use `erc-process'."(let((person(erc-personnickprocess)))(whenperson(erc-person-in-channelpersonchannel))))(defunerc-nick-channels(nick&optionalprocess)"Return the list of channels NICK is in.If PROCESS is nil, use `erc-process'."(let((person(erc-personnickprocess)))(whenperson;; someone was trying to pass in a second arg 'process'?(erc-person-channelsperson))))(defunerc-add-person-to-channel(personchannelmodes)"Add PERSON to CHANNEL with MODES.PERSON is an `erc-person' structure.CHANNEL is a string.MODES is a list of modes such as 'op or 'voice.If MODES is nil, the list (nil) will be used."(puthashchannel(ormodes'(nil))(erc-person-channelsperson));; when doing batch changes, bind erc-members-changed-hook to nil(run-hooks'erc-members-changed-hook))(defunerc-add-nick-to-channel(nickchannelmodes&optionalprocess)"Add NICK to CHANNEL for PROCESS with MODES.NICK is stringCHANNEL is a string.MODES is a list of modes such as 'op or 'voice, or nil.If PROCESS is nil, use `erc-process'."(let((nicks(gethashprocesserc-members))person)(unlessnicks(setqnicks(makehash'equal))(puthashprocessnickserc-members))(setqperson(erc-personnickprocess))(unlessperson(setqperson(make-erc-person:nicknick:channels(makehash'equal)))(puthash(erc-downcasenick)personnicks))(erc-add-person-to-channelpersonchannelmodes)))(defunerc-remove-person-from-channel(personchannel)"Remove PERSON from CHANNEL.PERSON is an `erc-person' structure.CHANNEL is a string."(remhashchannel(erc-person-channelsperson));; when doing batch changes, bind erc-members-changed-hook to nil(run-hooks'erc-members-changed-hook))(defunerc-remove-nick-from-channel(nickchannel&optionalprocess)"Remove NICK from CHANNEL for PROCESS.If PROCESS is nil, use `erc-process'."(erc-remove-person-from-channel(erc-personnickprocess)channel))(defunerc-person-get-mode-in-channel(personchannelmode)"Return non-nil if PERSON in CHANNEL has MODE.PERSON is an erc-person structure.CHANNEL is a string.MODE is a symbol such as `op'"(memqmode(gethashchannel(erc-person-channelsperson))))(defunerc-person-set-mode-in-channel(personchannelmodestatus)"Give PERSON MODE STATUS in CHANNEL.PERSON is an erc-person structure.MODE is the symbol `op' or `voice'.STATUS is nil or t.CHANNEL is a string."(let((modes(gethashchannel(erc-person-channelsperson))))(unlessmodes(error"%S is not in channel %s"personchannel))(ifstatus(or(memqmodemodes)(puthashchannel(consmodemodes)(erc-person-channelsperson)))(or(not(memqmodemodes))(puthashchannel(delqmodemodes)(erc-person-channelsperson))))))(defunerc-get-channel-members(channel&optionalprocess)"Return a list of erc-person structures for CHANNEL.If PROCESS is nil, use `erc-process'."(let(result)(maphash(lambda(nickperson)(when(erc-person-in-channelpersonchannel)(setqresult(conspersonresult))))(erc-membersprocess))result))(defunerc-refresh-channel-members(channelnames-string&optionaladd)"Update channel members for CHANNEL.All the nicks listed in NAMES-STRING are on that channel.If optional ADD is non-nil, do not remove existing names from the list.This refers to the channel named CHANNEL associated with the current`erc-process' only."(unlesserc-process(error"No erc-process in %S"(current-buffer)));; We need to delete "" because in XEmacs, (split-string "a ");; returns ("a" ""). Based on the nick names used in NAMES-STRING,;; we determine their op and voice capabilities, we create a;; hashtable where each key is the nick name, and the value has the;; form (OP VOICE), where OP and VOICE are either nil or t.(let((names(makehash))(erc-members-changed-hooknil)); bulk changes;; fill names table(dolist(name(delete""(split-stringnames-string)))(cond((string-match"^@\\(.*\\)$"name)(puthash(match-string1name)'(op)names))((string-match"^+\\(.*\\)$"name)(puthash(match-string1name)'(voice)names))(t(puthashnamenilnames))));; clear all channel members, if add is nil(unlessadd(mapc(lambda(person)(erc-remove-person-from-channelpersonchannel))(erc-get-channel-memberschannel)));; add all names now -- overwriting their previous modes(maphash(lambda(nickmodes)(erc-add-nick-to-channelnickchannelmodeserc-process))names))(run-hooks'erc-members-changed-hook))(defunerc-update-member(channelnick&optionalnew-nickaddopvoicehostemailfull-nameinfo);; when adding new arguments, be sure to check the test at the end;; before erc-members-changed-hook runs"Update the user info in the channel CHANNEL.All non-nil attributes will be used to update the info we have.The user's NICK will be changed to NEW-NICK. If ADD is non-nil, addthe user to CHANNEL. The other optional arguments OP, VOICE, HOST,EMAIL and FULL-NAME change the appropriate fields. INFO is theadditional info such as sign-on time or comments.Note: If OP or VOICE is nil, the status does not change, so use `on'or `off' to set the status instead of t and nil.If the info is actually updated, return non-nil and call`erc-channel-members-updated-hook'."(unlesserc-process(error"No erc-process in %S"(current-buffer)))(when(string=nicknew-nick)(setqnew-nicknil));; backwards compatibility(let((person(erc-personnick))(erc-members-changed-hooknil));; call it only once(erc-log(format"update-member: old %S"person))(whennew-nick(setf(erc-person-nickperson)new-nick)(remhashnick(erc-members))(puthash(erc-downcasenew-nick)person(erc-members)))(ifadd(let(modes)(when(eqop'on)(setqmodes(cons'opmodes)))(when(eqvoice'on)(setqmodes(cons'voicemodes)))(erc-add-person-to-channelpersonchannelmodes))(when(erc-person-in-channelpersonchannel)(whenop(erc-person-set-mode-in-channelpersonchannel'op(eqop'on)))(whenvoice(erc-person-set-mode-in-channelpersonchannel'op(eqvoice'on)))))(whenhost(setf(erc-person-hostperson)host))(whenemail(setf(erc-person-emailperson)email))(whenfull-name(setf(erc-person-full-nameperson)full-name))(wheninfo(setf(erc-person-infoperson)info))(erc-log(format"update-member: new %S"person)))(let((changes(ornew-nickaddopvoicehostemailfull-nameinfo)))(whenchanges(run-hooks'erc-members-changed-hook))changes))(make-obsolete'erc-update-channel-member'erc-update-member)(defalias'erc-update-channel-member'erc-update-member)(defunerc-buffer-list-with-nick(nick&optionalprocess)"Return buffers where NICK is online.If PROCESS is nil, use `erc-process'."(let((channels(erc-nick-channelsnickprocess)))(erc-buffer-filter(lambda()(member(erc-default-target)channels))process)));; FIXME: erc-format-nick and erc-format-@nick calling convention is;; not backwards compatible -- make a note of this! Search for other;; calls to these functions and fix them.(defunerc-format-nick(person)"Standard nickname formatting function.Returns the nick of PERSON.PERSON is an erc-person structure."(erc-person-nickperson))(defunerc-format-@nick(person)"Format a nickname such that @ or + are prefixed to the nick of PERSON,if OP or VOICE are t for the current `erc-default-target' respectively.PERSON is an erc-person structure."(let((channel(erc-default-target)))(whenchannel(concat(if(erc-person-get-mode-in-channelpersonchannel'voice)"+""")(if(erc-person-get-mode-in-channelpersonchannel'op)"@""")nick))))(defunerc-server-PRIVMSG-or-NOTICE(procparsed)(let((sspec(arefparsed1))(cmd(arefparsed0))(tgt(arefparsed2))(msg(arefparsed3)))(if(or(erc-ignored-user-psspec)(erc-ignored-reply-pmsgtgtproc))(iferc-minibuffer-ignored(message"Ignored %s from %s to %s"cmdsspectgt))(let*((sndr(erc-parse-usersspec))(nick(nth0sndr))(login(nth1sndr))(host(nth2sndr))(msgp(string=cmd"PRIVMSG"))(noticep(string=cmd"NOTICE"));; S.B. downcase *both* tgt and current nick(privp(erc-current-nick-ptgt))sbufferfnick)(setqbuffer(erc-get-buffer(ifprivpnicktgt)proc))(whenbuffer(with-current-bufferbuffer;; update the chat partner info. Add to the list if private;; message. We will accumulate private identities indefinitely;; at this point.(if(erc-update-channel-member(ifprivpnicktgt)nicknickprivpnilnilhostlogin)(erc-update-channel-info-buffer(ifprivpnicktgt)))(setqfnick(funcallerc-format-nick-function(erc-personnick)))))(cond((erc-is-message-ctcp-pmsg)(setqs(ifmsgp(erc-process-ctcp-queryprocparsednickloginhost)(erc-process-ctcp-replyprocparsednickloginhost(match-string1msg)))))(t(setcarlast-peersnick)(setqs(erc-format-privmessage(orfnicknick)msgprivpmsgp))))(whens(when(andnoticepprivperc-echo-notices-in-minibuffer-flag)(message(concat"NOTICE: "s)))(erc-display-messageparsednilbuffers))))))(defunerc-remove-channel-member(channelnick)"Remove NICK from CHANNEL in PROCESS.If PROCESS is nil, use `erc-process'."(erc-remove-nick-from-channelnickchannel))(make-obsolete'erc-remove-channel-member'erc-remove-nick-from-channel);; We should delete these stupid info buffers anyway. Improve our;; feature karma! Or at least move them out into a module that works;; using the erc-members-changed-hook.(make-obsolete'erc-update-channel-info-buffer'ignore)(make-obsolete'erc-channel-member-to-user-spec'erc-format-user)(defunerc-format-user(person)"Return a user string of the form nick!user@host for person.PERSON is an erc-person structure."(format"%s!%s@%s"(or(erc-person-nickperson)"")(or(erc-person-userperson)"")(or(erc-person-hostperson)"")))(defunerc-ignored-reply-p(messagetargetprocess)"Send MESSAGE to TARGET in PROCESS and maybe return return non-nil.We return non-nil, when MESSAGE is addressed to an ignored user, ie. a usermatching any regexp in `erc-ignore-reply-list'."(let((target-nick(erc-message-targetmessage)))(unlesstarget-nick(with-erc-channel-buffertargetprocess(when(erc-nick-in-channeltarget-nicktargetprocess)(erc-list-matcherc-ignore-reply-list(erc-format-user(erc-persontarget-nickprocess))))))));;; Testing;; Use (erc-members-debug) when looking at the data structure!;; I recommend M-x ielm for that.;(eval-when-compile; (let ((p (make-erc-person :channels (makehash 'equal))); (erc-process 'proc)); (erc-members-reset); (erc-add-person-to-channel p "foo" nil); (assert (erc-person-in-channel p "foo"));(assert (not (erc-person-in-channel p "bar")));(erc-person-set-mode-in-channel p "foo" 'op t);(assert (erc-person-get-mode-in-channel p "foo" 'op));(assert (not (erc-person-get-mode-in-channel p "foo" 'voice)));(assert (not (erc-person-get-mode-in-channel p "bar" 'op)));(erc-remove-person-from-channel p "foo");(assert (not (erc-person-in-channel p "foo")));(erc-refresh-channel-members "foo" "alex fritz @andi" t);(assert (equal '(nil) (erc-nick-in-channel "alex" "foo")));(assert (equal '(op) (erc-nick-in-channel "andi" "foo")));(erc-update-member "foo" "alex" "kensanata");(assert (not (erc-nick-in-channel "alex" "foo")));(assert (erc-nick-in-channel "kensanata" "foo"))));; FIXME: test if erc-buffer-list-with-nick returns query buffers, too;; FIXME: test what happens when a nick in a query buffer renames itself(provide'erc-members);;; erc-members.el ends here