# $Id: muc.tcl 1842 2009-10-26 18:50:13Z sergei $

# Multi-User Chat support (XEP-0045)

###############################################################################

namespace eval muc {
    set winid 0
    custom::defvar options(gen_enter_exit_msgs) 1 \
	[::msgcat::mc "Generate status messages when occupants\
	    enter/exit MUC compatible conference rooms."] \
	-type boolean -group Chat
    custom::defvar options(gen_muc_status_change_msgs) 0 \
	[::msgcat::mc "Generate groupchat messages when occupant\
	    changes his/her status and/or status message."] \
	-type boolean -group Chat
    custom::defvar options(gen_muc_position_change_msgs) 0 \
	[::msgcat::mc "Generate groupchat messages when occupant's\
	    room position (affiliation and/or role) changes."] \
	-type boolean -group Chat
    custom::defvar options(propose_configure) 0 \
	[::msgcat::mc "Propose to configure newly created MUC room.\
		       If set to false then the default room configuration\
		       is automatically accepted."] \
	-type boolean -group Chat
    custom::defvar options(history_maxchars) 10000 \
	[::msgcat::mc "Maximum number of characters in the history in MUC\
		       compatible conference rooms."] \
	-type integer -group Chat
    custom::defvar options(history_maxstanzas) 20 \
	[::msgcat::mc "Maximum number of stanzas in the history in MUC\
		       compatible conference rooms."] \
	-type integer -group Chat
    custom::defvar options(request_only_unseen_history) 0 \
	[::msgcat::mc "Request only unseen (which aren't displayed in the\
		       chat window) messages in the history in MUC compatible\
		       conference rooms."] \
	-type boolean -group Chat
    custom::defvar options(report_muc_rooms) 0 \
	[::msgcat::mc "Report the list of current MUC rooms on\
		       disco#items query."] \
	-type boolean -group IQ

    # MUC affiliations and roles:

    array set e2l [list owner       [::msgcat::mc "owner"] \
			admin       [::msgcat::mc "admin"] \
			member      [::msgcat::mc "member"] \
			outcast     [::msgcat::mc "outcast"] \
			none        [::msgcat::mc "none"] \
			moderator   [::msgcat::mc "moderator"] \
			participant [::msgcat::mc "participant"] \
			visitor     [::msgcat::mc "visitor"]]

    variable maxl 0
    foreach n [array names e2l] {
	set l2e($e2l($n)) $n
	set l [string length $e2l($n)]
	if {$l + 2 > $maxl} {
	    set maxl [expr {$l + 2}]
	}
    }
}

###############################################################################

set ::NS(muc)       http://jabber.org/protocol/muc
set ::NS(muc#admin) http://jabber.org/protocol/muc#admin
set ::NS(muc#owner) http://jabber.org/protocol/muc#owner
set ::NS(muc#user)  http://jabber.org/protocol/muc#user

set ::NS(muc#rooms)  http://jabber.org/protocol/muc#rooms

###############################################################################

proc muc::add_groupchat_user_menu_items {type m xlib jid} {
    set group [::xmpp::jid::stripResource $jid]

    if {![is_compatible $group]} return

    set chatid [chat::chatid $xlib $group]

    if {![chat::is_groupchat $chatid]} {
	return
    }

    switch -- $type {
	chat { set reschatid [chat::chatid $xlib $jid] }
	roster { set reschatid $chatid }
    }

    set mm [menu $m.muc -tearoff 0]
    $mm add command -label [::msgcat::mc "Whois"] \
	-command [list muc::whois $xlib $jid $reschatid]
    $mm add command -label [::msgcat::mc "Kick"] \
	-command [list muc::change_item_param \
		      {role none} down $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Ban"] \
	-command [list muc::change_item_param \
		      {affiliation outcast} down $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Grant Voice"] \
	-command [list muc::change_item_param \
		      {role participant} up $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Revoke Voice"] \
	-command [list muc::change_item_param \
		      {role visitor} down $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Grant Membership"] \
	-command [list muc::change_item_param \
		      {affiliation member} up $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Revoke Membership"] \
	-command [list muc::change_item_param \
		      {affiliation none} down $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Grant Moderator Privileges"] \
	-command [list muc::change_item_param \
		      {role moderator} up $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Revoke Moderator Privileges"] \
	-command [list muc::change_item_param \
		      {role participant} down $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Grant Admin Privileges"] \
	-command [list muc::change_item_param \
		      {affiliation admin} up $xlib $jid "" $reschatid]
    $mm add command -label [::msgcat::mc "Revoke Admin Privileges"] \
	-command [list muc::change_item_param \
		      {affiliation member} down $xlib $jid "" $reschatid]
    #$mm add command -label [::msgcat::mc "Grant Owner Privileges"] \
    #    -command [list muc::change_item_param \
    #    	      {affiliation owner} up $xlib $jid "" $reschatid]
    #$mm add command -label [::msgcat::mc "Revoke Owner Privileges"] \
    #    -command [list muc::change_item_param \
    #		      {affiliation admin} down $xlib $jid "" $reschatid]

    $m add cascade -label [::msgcat::mc "MUC"] -menu $mm
}

hook::add chat_create_user_menu_hook \
	  [list muc::add_groupchat_user_menu_items chat] 43
hook::add roster_create_groupchat_user_menu_hook \
	  [list muc::add_groupchat_user_menu_items roster] 39

###############################################################################

proc muc::add_muc_menu_items {m xlib group {state disabled}} {
    if {[is_compatible $group]} {
	set state normal
    }

    set chatid [chat::chatid $xlib $group]

    set mm [menu $m.muc -tearoff 0]

    $mm add command -label [::msgcat::mc "Configure room"] \
	-command [list muc::request_config $chatid]
    $mm add command -label [::msgcat::mc "Edit voice list"] \
	-command [list muc::request_list role participant $chatid]
    $mm add command -label [::msgcat::mc "Edit ban list"] \
	-command [list muc::request_list affiliation outcast $chatid]
    $mm add command -label [::msgcat::mc "Edit member list"] \
	-command [list muc::request_list affiliation member $chatid]
    $mm add command -label [::msgcat::mc "Edit moderator list"] \
	-command [list muc::request_list role moderator $chatid]
    $mm add command -label [::msgcat::mc "Edit admin list"] \
	-command [list muc::request_list affiliation admin $chatid]
    $mm add command -label [::msgcat::mc "Edit owner list"] \
	-command [list muc::request_list affiliation owner $chatid]
    $mm add separator
    $mm add command -label [::msgcat::mc "Destroy room"] \
	-command [list muc::request_destruction_dialog $chatid "" ""]

    $m add cascade -label [::msgcat::mc "MUC"] -menu $mm -state $state
}

hook::add chat_create_conference_menu_hook muc::add_muc_menu_items 37

proc muc::disco_node_menu_setup {m bw tnode data parentdata} {
    lassign $data type xlib jid node
    switch -- $type {
	item -
	item2 {
	    set identities [disco::browser::get_identities $bw $tnode]

	    if {[lempty $identities]} {
		set identities [disco::browser::get_parent_identities $bw $tnode]
	    }

	    set features [disco::browser::get_features $bw $tnode]

	    if {[lempty $features]} {
		set features [disco::browser::get_parent_features $bw $tnode]
	    }

	    # JID with resource is not a room JID
	    if {[::xmpp::jid::stripResource $jid] != $jid} return

	    # A room must have non-empty node
	    if {[::xmpp::jid::node $jid] == ""} return

	    foreach id $identities {
		if {[::xmpp::xml::getAttr $id category] == "conference"} {
		    foreach f $features {
			if {$f == $::NS(muc)} {
			    add_muc_menu_items $m $xlib $jid normal
			    return
			}
		    }
		}
	    }
	}
    }
}

hook::add disco_node_menu_hook muc::disco_node_menu_setup 60

###############################################################################

proc muc::handle_commands {chatid user body type} {
    if {$type != "groupchat"} return

    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]
    if {[cequal [crange $body 0 5] "/kick "]} {
	set params {role none}
	set dir down
	lassign [parse_nick_reason $body 6] nick reason
    } elseif {[cequal [crange $body 0 4] "/ban "]} {
	set params {affiliation outcast}
	set dir down
	lassign [parse_nick_reason $body 5] nick reason
    } elseif {[cequal [crange $body 0 6] "/unban "]} {
	set jid [parse_nick $body 7]
	unban $xlib $group $jid
	return stop
    } elseif {[cequal [crange $body 0 6] "/whois "]} {
	set nick [parse_nick $body 7]
	whois $xlib $group/$nick $chatid
	return stop
    } elseif {[cequal [crange $body 0 6] "/voice "]} {
	set params {role participant}
	set dir up
	lassign [parse_nick_reason $body 7] nick reason
    } elseif {[cequal [crange $body 0 8] "/devoice "]} {
	set params {role visitor}
	set dir down
	lassign [parse_nick_reason $body 9] nick reason
    } elseif {[cequal [crange $body 0 7] "/member "]} {
	set params {affiliation member}
	set dir up
	lassign [parse_nick_reason $body 8] nick reason
    } elseif {[cequal [crange $body 0 9] "/demember "]} {
	set params {affiliation none}
	set dir down
	lassign [parse_nick_reason $body 10] nick reason
    } elseif {[cequal [crange $body 0 10] "/moderator "]} {
	set params {role moderator}
	set dir up
	lassign [parse_nick_reason $body 11] nick reason
    } elseif {[cequal [crange $body 0 12] "/demoderator "]} {
	set params {role participant}
	set dir down
	lassign [parse_nick_reason $body 13] nick reason
    } elseif {[cequal [crange $body 0 6] "/admin "]} {
	set params {affiliation admin}
	set dir up
	lassign [parse_nick_reason $body 7] nick reason
    } elseif {[cequal [crange $body 0 8] "/deadmin "]} {
	set params {affiliation member}
	set dir down
	lassign [parse_nick_reason $body 9] nick reason
    } else {
	return
    }
    
    change_item_param $params $dir $xlib $group/$nick $reason $chatid

    return stop
}

hook::add chat_send_message_hook muc::handle_commands 50

proc muc::parse_nick {body n} {
    return [lindex [parse_nick_reason $body $n] 0]
}

proc muc::parse_nick_reason {body n} {
    # Parse nickname and reason
    # first line is a nick, rest are reason
    set nick_reason [crange $body $n end]
    set ne [string first "\n" $nick_reason]
    if {$ne < 0} {
	set nick $nick_reason
	set reason ""
    } else {
	set nick [string range $nick_reason 0 [expr {$ne - 1}]]
	set reason [string range $nick_reason [expr {$ne + 1}] end]
    }
    return [list $nick [string trim $reason]]
}

###############################################################################

proc muc::commands_comps {chatid compsvar wordstart line} {
    set group [chat::get_jid $chatid]
    if {![is_compatible $group]} return

    upvar 0 $compsvar comps

    if {!$wordstart} {
	lappend comps {/whois } {/kick } {/ban } {/unban } \
	    {/voice } {/devoice } \
	    {/member } {/demember } \
	    {/moderator } {/demoderator } \
	    {/admin } {/deadmin }
    }
}

hook::add generate_completions_hook muc::commands_comps

###############################################################################

proc muc::get_real_jid {xlib jid} {
    variable users

    if {[info exists users(jid,$xlib,$jid)] && \
	    $users(jid,$xlib,$jid) != ""} {
	return $users(jid,$xlib,$jid)
    } else {
	return ""
    } 
}

proc muc::whois {xlib user reschatid} {
    set group [::xmpp::jid::stripResource $user]
    set chatid [chat::chatid $xlib $group]
    set nick [chat::get_nick $xlib $user groupchat]

    set real_jid [get_real_jid $xlib $user]

    if {$real_jid != ""} {
	chat::add_message $reschatid $group info \
	    [::msgcat::mc "whois '%s': %s" $nick $real_jid] {}
    } else {
	chat::add_message $reschatid $group error \
	    [::msgcat::mc "whois '%s': no info" $nick] {}
    }
}

###############################################################################

proc muc::compare_roles {role1 role2} {
    set roles {none visitor participant moderator}

    set idx1 [lsearch -exact $roles $role1]
    set idx2 [lsearch -exact $roles $role2]
    expr {$idx1 - $idx2}
}

proc muc::compare_affs {aff1 aff2} {
    set affs {outcast none member admin owner}

    set idx1 [lsearch -exact $affs $aff1]
    set idx2 [lsearch -exact $affs $aff2]
    expr {$idx1 - $idx2}
}

proc muc::change_item_param {params dir xlib user reason reschatid} {
    variable users

    set group [::xmpp::jid::stripResource $user]
    set chatid [chat::chatid $xlib $group]
    set nick  [chat::get_nick $xlib $user groupchat]

    lassign $params key val
    if {![catch { set aff $users(affiliation,$xlib,$user) }] && \
	    ![catch { set role $users(role,$xlib,$user) }]} {
	switch -- $key/$dir {
	    affiliation/up {
		if {[compare_affs $aff $val] >= 0} {
		    chat::add_message $reschatid $group error \
			"affiliation $val '$nick':\
			 [::msgcat::mc {User already %s} $aff]" {}
		    return
		}
	    }
	    affiliation/down {
		if {[compare_affs $aff $val] <= 0} {
		    chat::add_message $reschatid $group error \
			"affiliation $val '$nick':\
			 [::msgcat::mc {User already %s} $aff]" {}
		    return
		}
	    }
	    role/up {
		if {[compare_roles $role $val] >= 0} {
		    chat::add_message $reschatid $group error \
			"role $val '$nick':\
			 [::msgcat::mc {User already %s} $role]" {}
		    return
		}
	    }
	    role/down {
		if {[compare_roles $role $val] <= 0} {
		    chat::add_message $reschatid $group error \
			"role $val '$nick':\
			 [::msgcat::mc {User already %s} $role]" {}
		    return
		}
	    }
	    default {
		return
	    }
	}
    }

    set itemsubtags {}
    if {$reason != ""} {
	lappend itemsubtags [::xmpp::xml::create reason -cdata $reason]
    }
    set vars [list nick $nick]
    if {$params == {affiliation outcast}} {
	# For unknown reason banning request MUST be based on
	# user's bare JID (which may be not known by admin)
	set real_jid [get_real_jid $xlib $user]
	if {$real_jid != ""} {
	    set vars [list jid [::xmpp::jid::stripResource $real_jid]]
	}
	
    }
    set item [::xmpp::xml::create item \
		  -attrs [concat $vars $params] \
		  -subelements $itemsubtags]

    ::xmpp::sendIQ $xlib set \
	-query [::xmpp::xml::create query \
			-xmlns $::NS(muc#admin) \
			-subelement $item] \
	-to $group \
	-command [list muc::test_error_res "$params '$nick'" $xlib $group $reschatid]
}

###############################################################################

proc muc::unban {xlib group jid} {
    ::xmpp::sendIQ $xlib get \
	-query [::xmpp::xml::create query \
			-xmlns $::NS(muc#admin) \
			-subelement [::xmpp::xml::create item \
					    -attrs {affiliation outcast}]] \
	-to $group \
	-command [list muc::unban_continue $xlib $group $jid]
}

proc muc::unban_continue {xlib group jid res child} {
    if {$res != "ok"} {
	chat::add_message [chat::chatid $xlib $group] $group error \
	    "affiliation none '$jid': [error_to_string $child]" {}
	return
    }

    ::xmpp::xml::split $child tag xmlns attrs cdata subels

    set jid [::xmpp::jid::stripResource $jid]
    set found 0
    foreach item $subels {
	::xmpp::xml::split $item stag sxmlns sattrs scdata ssubels
	switch -- $stag {
	    item {
		set jid1 [::xmpp::xml::getAttr $sattrs jid]
		if {$jid == $jid1} {
		    set found 1
		    break
		}
	    }
	}
    }

    if {!$found} {
	chat::add_message [chat::chatid $xlib $group] $group error \
	    "affiliation none '$jid': User is not banned" {}
	return
    }

    set item [::xmpp::xml::create item \
		  -attrs [list jid $jid affiliation none]]

    ::xmpp::sendIQ $xlib set \
	-query [::xmpp::xml::create query \
			-xmlns $::NS(muc#admin) \
			-subelement $item] \
	-to $group \
	-command [list muc::test_unban_res $xlib $group $jid]
}

proc muc::test_unban_res {xlib group jid res child} {
    if {$res != "ok"} {
	chat::add_message [chat::chatid $xlib $group] $group error \
	    "affiliation none '$jid': [error_to_string $child]" {}
	return
    }

    chat::add_message [chat::chatid $xlib $group] $group info \
	"affiliation none $jid: User is unbanned" {}
}

###############################################################################

proc muc::request_destruction_dialog {chatid alt reason} {
    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]

    set warning \
	[::msgcat::mc "Conference room %s will be destroyed\
		       permanently.\n\nProceed?" $group]

    set res [MessageDlg .muc_request_destruction -aspect 50000 -icon warning \
			-type user -buttons {yes no} -default 1 \
			-cancel 1 \
			-message $warning]
    if {!$res} {
	request_destruction $chatid $alt $reason
    }
}

proc muc::request_destruction {chatid alt reason} {
    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]

    if {$alt != ""} {
	set vars [list jid $alt]
    } else {
	set vars {}
    }

    ::xmpp::sendIQ $xlib set \
	-query [::xmpp::xml::create query \
		    -xmlns $::NS(muc#owner) \
		    -subelement [::xmpp::xml::create destroy \
					-attrs $vars \
					-subelement [::xmpp::xml::create reason \
							    -cdata $reason]]] \
	-to $group \
	-command [list muc::test_error_res "destroy" $xlib $group $chatid]
}

###############################################################################

proc muc::request_list {attr val chatid} {
    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]
    ::xmpp::sendIQ $xlib get \
	-query [::xmpp::xml::create query \
			-xmlns $::NS(muc#admin) \
			-subelement [::xmpp::xml::create item \
					-attrs [list $attr $val]]] \
	-to $group \
	-command [list muc::receive_list $attr $val $chatid]
}


proc muc::receive_list {attr val chatid res child} {
    variable e2l
    variable maxl

    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]
    if {![string equal $res ok]} {
	chat::add_message $chatid $group error \
	    "$attr $val list: [error_to_string $child]" {}
	return
    }

    ::xmpp::xml::split $child tag xmlns attrs cdata subels

    variable winid

    set w .muc_list$winid
    incr winid

    if {[winfo exists $w]} {
	destroy $w
    }

    # [format] is intentional here. After %s substitution, it becomes
    # one of the following:
    # [::msgcat::mc "Edit owner list"]
    # [::msgcat::mc "Edit admin list"]
    # [::msgcat::mc "Edit member list"]
    # [::msgcat::mc "Edit outcast list"]
    # [::msgcat::mc "Edit moderator list"]
    # [::msgcat::mc "Edit participant list"]
    # [::msgcat::mc "Edit visitor list"]
    Dialog $w -title [::msgcat::mc [format "Edit %s list" $val]] \
        -modal none -separator 1 -anchor e -default 0 -cancel 1

    set wf [$w getframe]

    set sw [ScrolledWindow $wf.sw]

    set lb [::mclistbox::mclistbox $sw.listbox \
		    -exportselection 0 \
		    -resizeonecolumn 1 \
		    -labelanchor w \
		    -width 90 \
		    -height 16]

    bind $lb <Destroy> [namespace code [list list_cleanup %W]]

    bind $lb <<ListboxSelect>> [namespace code [list update_fields %W [double% $val] [double% $w.fr1]]]

    $sw setwidget $lb
    fill_list $lb $subels $attr $val
 
    $w add -text [::msgcat::mc "Send"] \
	-command [list muc::send_list $chatid $attr $val $w $lb]
    $w add -text [::msgcat::mc "Cancel"] -command [list destroy $w]


    frame $w.fr1
    pack $w.fr1 -side bottom -in $wf -fill x -pady 1m

    grid columnconfigure $w.fr1 3 -weight 1

    label $w.fr1.lnick -text [::msgcat::mc "Nick"]
    label $w.fr1.ljid -text [::msgcat::mc "JID"]
    switch -- $attr {
	role {
	    label $w.fr1.lattr -text [::msgcat::mc "Role"]
	}
	affiliation {
	    label $w.fr1.lattr -text [::msgcat::mc "Affiliation"]
	}
    }
    label $w.fr1.lreason -text [::msgcat::mc "Reason"]

    grid $w.fr1.lnick   -row 0 -column 0 -sticky w
    grid $w.fr1.ljid    -row 0 -column 1 -sticky w
    grid $w.fr1.lattr   -row 0 -column 2 -sticky w
    grid $w.fr1.lreason -row 0 -column 3 -sticky w

    entry $w.fr1.nick1 -takefocus 0 -highlightthickness 0
    if {[catch {$w.fr1.nick1 configure -state readonly}]} {
	$w.fr1.nick1 configure -state disabled
    }
    entry $w.fr1.jid1 -takefocus 0 -highlightthickness 0
    if {[catch {$w.fr1.jid1 configure -state readonly}]} {
	$w.fr1.jid1 configure -state disabled
    }

    switch -- $attr {
	role {
	    ComboBox $w.fr1.attr1 -text $e2l($val) \
		-values [list $e2l(moderator) $e2l(participant) $e2l(visitor) $e2l(none)] \
		-editable no \
		-width $maxl
	}
	affiliation {
	    ComboBox $w.fr1.attr1 -text $e2l($val) \
		-values [list $e2l(owner) $e2l(admin) $e2l(member) $e2l(none) $e2l(outcast)] \
		-editable no \
		-width $maxl
	}
    }

    entry $w.fr1.reason1

    button $w.fr1.update -text [::msgcat::mc "Update item"] \
	-command [list muc::list_update_item $lb $attr $w.fr1]

    grid $w.fr1.nick1   -row 1 -column 0 -sticky ew
    grid $w.fr1.jid1    -row 1 -column 1 -sticky ew
    grid $w.fr1.attr1   -row 1 -column 2 -sticky ew
    grid $w.fr1.reason1 -row 1 -column 3 -sticky ew
    grid $w.fr1.update  -row 1 -column 4 -sticky ew

    entry $w.fr1.nick2
    entry $w.fr1.jid2

    switch -- $attr {
	role {
	    ComboBox $w.fr1.attr2 -text $e2l($val) \
		-values [list $e2l(moderator) $e2l(participant) $e2l(visitor) $e2l(none)] \
		-editable no \
		-width $maxl
	}
	affiliation {
	    ComboBox $w.fr1.attr2 -text $e2l($val) \
		-values [list $e2l(owner) $e2l(admin) $e2l(member) $e2l(none) $e2l(outcast)] \
		-editable no \
		-width $maxl
	}
    }

    entry $w.fr1.reason2

    button $w.fr1.add -text [::msgcat::mc "Add new item"] \
	-command [list muc::list_add_item $lb $val $w.fr1]

    grid $w.fr1.nick2   -row 2 -column 0 -sticky ew
    grid $w.fr1.jid2    -row 2 -column 1 -sticky ew
    grid $w.fr1.attr2   -row 2 -column 2 -sticky ew
    grid $w.fr1.reason2 -row 2 -column 3 -sticky ew
    grid $w.fr1.add     -row 2 -column 4 -sticky ew

    frame $w.fr1.fr

    label $w.lall -text [::msgcat::mc "All items:"]
    pack $w.lall -side left -in $w.fr1.fr -padx 1m -pady 1m

    switch -- $attr {
	role {
	    ComboBox $w.roleall -text $e2l($val) \
		-values [list $e2l(moderator) $e2l(participant) $e2l(visitor) $e2l(none)] \
		-editable no \
		-width $maxl \
		-modifycmd [list muc::change_all_items $lb $w.roleall $attr]
	    pack $w.roleall -side left -anchor w -in $w.fr1.fr -pady 1m
	}
	affiliation {
	    ComboBox $w.affiliationall -text $e2l($val) \
		-values [list $e2l(owner) $e2l(admin) $e2l(member) $e2l(none) $e2l(outcast)] \
		-editable no \
		-width $maxl \
		-modifycmd [list muc::change_all_items $lb $w.affiliationall $attr]
	    pack $w.affiliationall -side left -in $w.fr1.fr -pady 1m
	}
    }

    grid $w.fr1.fr -row 3 -column 0 -columnspan 5 -sticky w

    pack $sw -side top -expand yes -fill both

    bindscroll $sw $lb

    $w draw
}

###############################################################################

proc muc::trim {str} {
    string range $str 1 end-1
}

proc muc::untrim {str} {
    return " $str "
}

proc muc::update_fields {lb val fr} {
    set selection [$lb curselection]
    if {[llength $selection] == 0} {
	set nick ""
	set jid ""
	set attr $val
	set reason ""
    } else {
	set data [$lb get [lindex $selection 0]]
	lassign $data n nick jid attr reason
    }

    $fr.nick1 configure -state normal
    $fr.nick1 delete 0 end
    $fr.nick1 insert 0 [trim $nick]
    if {[catch {$fr.nick1 configure -state readonly}]} {
	$fr.nick1 configure -state disabled
    }
    $fr.jid1 configure -state normal
    $fr.jid1 delete 0 end
    $fr.jid1 insert 0 [trim $jid]
    if {[catch {$fr.jid1 configure -state readonly}]} {
	$fr.jid1 configure -state disabled
    }
    $fr.attr1 configure -text [trim $attr]
    $fr.reason1 delete 0 end
    $fr.reason1 insert 0 [trim $reason]
}

proc muc::list_update_item {lb attr fr} {
    set selection [$lb curselection]
    if {[llength $selection] == 0} {
	return
    }
    set idx [lindex $selection 0]
    set data [lrange [$lb get $idx] 0 0]

    lappend data [untrim [$fr.nick1 get]] \
		 [untrim [$fr.jid1 get]] \
		 [untrim [$fr.attr1 get]] \
		 [untrim [$fr.reason1 get]]

    incr idx
    $lb insert $idx $data
    incr idx -1
    $lb delete $idx
    $lb selection clear 0 end
    $lb selection set $idx
}

proc muc::change_all_items {lb combobox attr} {
    set value [$combobox get]

    set yposition [lindex [$lb yview] 0]
    set data [$lb get 0 end]
    set result {}
    set i 0
    foreach row $data {
	lappend result [lreplace $row 3 3 [untrim $value]]
    }
    $lb delete 0 end
    eval $lb insert end $result
    $lb yview moveto $yposition
}

###############################################################################

proc muc::fill_list {lb items attr val} {
    variable listdata
    variable lastsort
    variable e2l
    variable maxl

    set lastsort($lb) jid

    set width(0) 3
    set name(0) n
    $lb column add n -label [untrim [::msgcat::mc "#"]]

    set width(1) [expr {[string length [::msgcat::mc "Nick"]] + 2}]
    set name(1) nick
    $lb column add nick -label [untrim [::msgcat::mc "Nick"]]
    $lb label bind nick <ButtonPress-1> [namespace code [list Sort %W nick]]

    set width(2) [expr {[string length [::msgcat::mc "JID"]] + 2}]
    set name(2) jid
    $lb column add jid -label [untrim [::msgcat::mc "JID"]]
    $lb label bind jid <ButtonPress-1> [namespace code [list Sort %W jid]]

    switch -- $attr {
	role {
	    set width(3) [expr {[string length [::msgcat::mc "Role"]] + 2}]
	    if {$width(3) < $maxl} {
		set width(3) $maxl
	    }
	    set name(3) role
	    $lb column add role -label [untrim [::msgcat::mc "Role"]]
	    $lb label bind role <ButtonPress-1> [namespace code [list Sort %W role]]
	}
	affiliation {
	    set width(3) [expr {[string length [::msgcat::mc "Affiliation"]] + 2}]
	    if {$width(3) < $maxl} {
		set width(3) $maxl
	    }
	    set name(3) affiliation
	    $lb column add affiliation -label [untrim [::msgcat::mc "Affiliation"]]
	    $lb label bind affiliation <ButtonPress-1> [namespace code [list Sort %W affiliation]]
	}
    }

    set width(4) [expr {[string length [::msgcat::mc "Reason"]] + 2}]
    set name(4) reason
    $lb column add reason -label [untrim [::msgcat::mc "Reason"]]
    $lb label bind reason <ButtonPress-1> [namespace code [list Sort %W reason]]

    $lb column add lastcol -label "" -width 0
    $lb configure -fillcolumn lastcol
    
    set items2 {}
    foreach item $items {
	::xmpp::xml::split $item tag xmlns attrs cdata subels
	switch -- $tag {
	    item {
		set nick [::xmpp::xml::getAttr $attrs nick]
		set jid [::xmpp::xml::getAttr $attrs jid]
		switch -- $attr {
		    role {
			set attribute [::xmpp::xml::getAttr $attrs role]
		    }
		    affiliation {
			set attribute [::xmpp::xml::getAttr $attrs affiliation]
		    }
		}
		set reason ""
		foreach subitem $subels {
		    ::xmpp::xml::split $subitem stag sxmlns sattrs scdata ssubels
		    if {$stag == "reason"} {
			set reason $scdata
		    }
		}
		lappend items2 [list $nick $jid $e2l($attribute) $reason]
	    }
	}
    }

    set listdata($lb) [lsort -dictionary -index 1 $items2]
    set row 1
    foreach item $listdata($lb) {
	set itemdata {}
	set i 0
	foreach data [linsert $item 0 $row] {
	    set wd [string length [untrim $data]]
	    if {$wd > $width($i)} {
		if {$wd > 50} {
		    set width($i) 50
		} else {
		    set width($i) $wd
		}
	    }
	    lappend itemdata [untrim $data]
	    incr i
	}
	lappend itemdata ""
	$lb insert end $itemdata

	incr row
    }

    for {set i 0} {$i < 5} {incr i} {
	$lb column configure $name($i) -width $width($i)
    }
}

proc muc::Sort {lb tag} {
    variable lastsort

    set data [$lb get 0 end]
    set index [lsearch -exact [$lb column names] $tag]
    if {$lastsort($lb) != $tag} {
	set result [lsort -dictionary -index $index $data]
	set lastsort($lb) $tag
    } else {
	set result [lsort -decreasing -dictionary -index $index $data]
	set lastsort($lb) ""
    }
    set result1 {}
    set i 0
    foreach row $result {
	lappend result1 [lreplace $row 0 0 [untrim [incr i]]]
    }
    $lb delete 0 end
    eval $lb insert end $result1
}

###############################################################################

proc muc::list_add_item {lb val fr} {
    variable e2l

    set n [trim [lindex [$lb get end] 0]]
    if {[string equal $n ""]} {
	set n 0
    }

    set data [list [untrim [incr n]] \
	     [untrim [$fr.nick2 get]] \
	     [untrim [$fr.jid2 get]] \
	     [untrim [$fr.attr2 get]] \
	     [untrim [$fr.reason2 get]]]

    $lb insert end $data

    $fr.nick2 delete 0 end
    $fr.jid2 delete 0 end
    $fr.attr2 configure -text $e2l($val)
    $fr.reason2 delete 0 end
}

###############################################################################

proc muc::send_list {chatid attr val w lb} {
    variable listdata
    variable l2e

    foreach item $listdata($lb) {
	set tmp([lrange $item 0 1]) $item
    }

    set items {}
    foreach item [$lb get 0 end] {
	lassign $item n nick jid val reason
	set nick [trim $nick]
	set jid [trim $jid]

	if {$nick == "" && $jid == ""} {
	    continue
	}

	set val [trim $val]
	set reason [trim $reason]

	if {[info exists tmp([list $nick $jid])] && \
		$tmp([list $nick $jid]) == [list $nick $jid $val $reason]} continue

	set attrs [list $attr $l2e($val)]
	if {$nick != ""} {
	    lappend attrs nick $nick
	}
	if {$jid != ""} {
	    lappend attrs jid $jid
	}
	set itemsubels {}
	if {$reason != ""} {
	    lappend itemsubels [::xmpp::xml::create reason -cdata $reason]
	}
	lappend items [::xmpp::xml::create item \
				-attrs $attrs \
				-subelements $itemsubels]
    }

    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]

    if {$items != {}} {
	::xmpp::sendIQ $xlib set \
	    -query [::xmpp::xml::create query \
			       -xmlns $::NS(muc#admin) \
			       -subelements $items] \
	    -to $group \
	    -command [list muc::test_error_res \
			   [::msgcat::mc "Sending %s %s list" $attr $val] \
			   $xlib $group $chatid]
    }
    destroy $w
}

proc muc::list_cleanup {f} {
    variable listdata
    variable lastsort

    catch {unset listdata($f)}
    catch {unset lastsort($f)}
}

###############################################################################

proc muc::request_config_dialog {chatid} {
    variable winid

    set w .muc_req_config$winid
    incr winid

    if {[winfo exists $w]} {
	destroy $w
    }

    Dialog $w -title [::msgcat::mc "Room is created"] \
	-modal none -separator 1 -anchor e -default 0 -cancel 1

    set wf [$w getframe]
    message $wf.message -aspect 50000 \
	-text [::msgcat::mc "Room %s is successfully created" \
		    [chat::get_jid $chatid]]

    pack $wf.message -pady 2m

    $w add -text [::msgcat::mc "Configure room"] \
	-command "[list destroy $w]
		  [list [namespace current]::request_config $chatid]"
    $w add -text [::msgcat::mc "Accept default config"] \
	-command "[list destroy $w]
		  [list [namespace current]::request_instant_room $chatid]"

    $w draw
    
}

proc muc::request_instant_room {chatid} {
    ::xmpp::sendIQ [chat::get_xlib $chatid] set \
	-query [::xmpp::xml::create query \
			-xmlns $::NS(muc#owner) \
			-subelement [::xmpp::data::submitForm {}]] \
	-to [chat::get_jid $chatid]
}

proc muc::request_config {chatid} {
    ::xmpp::sendIQ [chat::get_xlib $chatid] get \
	-query [::xmpp::xml::create query \
			-xmlns $::NS(muc#owner)] \
	-to [chat::get_jid $chatid] \
	-command [list muc::receive_config $chatid]
}

proc muc::receive_config {chatid res child} {
    set group [chat::get_jid $chatid]
    if {![string equal $res ok]} {
	chat::add_message $chatid $group error \
	    [::msgcat::mc "Configure form: %s" [error_to_string $child]] \
	    {}
	return
    }

    ::xmpp::xml::split $child tag xmlns attrs cdata subels

    data::draw_window $subels [list muc::send_config $chatid] \
			      [list muc::cancel_config $chatid]
    return
}

###############################################################################

proc muc::send_config {chatid w restags} {
    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]
    ::xmpp::sendIQ $xlib set \
	-query [::xmpp::xml::create query \
			   -xmlns $::NS(muc#owner) \
			   -subelements $restags] \
	-to $group \
	-command [list muc::test_error_res \
		       [::msgcat::mc "Sending configure form"] $xlib $group $chatid]
    destroy $w
}

###############################################################################

proc muc::cancel_config {chatid w} {
    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]
    ::xmpp::sendIQ $xlib set \
	-query [::xmpp::xml::create query \
			   -xmlns $::NS(muc#owner) \
			   -subelement [::xmpp::data::cancelForm]] \
	-to $group \
	-command [list muc::test_error_res \
		       [::msgcat::mc "Cancelling configure form"] $xlib $group $chatid]
    destroy $w
}

###############################################################################

proc muc::test_error_res {op xlib group reschatid res child} {
    if {![string equal $res ok]} {
	set chatid [chat::chatid $xlib $group]
	chat::add_message $reschatid $group error \
	    [format "%s: %s" $op [error_to_string $child]] {}
	return
    }
}

###############################################################################

proc muc::process_presence {xlib from type x args} {
    switch -- $type {
	available -
	unavailable {
	    foreach xs $x {
		::xmpp::xml::split $xs tag xmlns attrs cdata subels
		if {$xmlns == $::NS(muc#user)} {
		    process_muc_user $xlib $from $type $subels
		}
	    }
	}
    }
}

hook::add client_presence_hook muc::process_presence

###############################################################################

proc muc::process_muc_user {xlib user type children} {
    variable users
    variable options

    set nick [chat::get_nick $xlib $user groupchat]
    set group [::xmpp::jid::stripResource $user]
    set chatid [chat::chatid $xlib $group]

    foreach child $children {
	::xmpp::xml::split $child tag xmlns attrs cdata subels
	switch -- $tag {
	    item {
		if {$type != "unavailable"} {
		    set users(jid,$xlib,$user) [::xmpp::xml::getAttr $attrs jid]
		    track_room_position $xlib $user \
			[::xmpp::xml::getAttr $attrs role] \
			[::xmpp::xml::getAttr $attrs affiliation]
		} else {
		    set new_nick [::xmpp::xml::getAttr $attrs nick]
		    foreach ch $subels {
			::xmpp::xml::split $ch stag sxmlns sattrs scdata ssubels
			switch -- $stag {
			    reason {
				set reason $scdata
			    }
			    actor {
				set actor [::xmpp::xml::getAttr $sattrs jid]
			    }
			}
		    }
		}
	    }
	    destroy {
		set msg [::msgcat::mc "Room is destroyed"]
		foreach ch $subels {
		    ::xmpp::xml::split $ch stag sxmlns sattrs scdata ssubels
		    if {$stag == "reason" && [string trim $scdata] != ""} {
			append msg [::msgcat::mc "\nReason: %s" [string trim $scdata]]
		    }
		}
		set altjid [::xmpp::xml::getAttr $attrs jid]
		if {$altjid != ""} {
		    append msg [::msgcat::mc "\nAlternative venue: %s" $altjid]
		}
		if {$options(gen_enter_exit_msgs)} {
		    chat::add_message \
			$chatid $group groupchat $msg {}
		}
	    }
	}
    }

    foreach child $children {
	::xmpp::xml::split $child tag xmlns attrs cdata subels
	switch -- $tag {
	    status {
		set code [::xmpp::xml::getAttr $attrs code]
		switch -- $code {
		    110 -
		    210 {
			# 110: This present packet is our own
			# 210: The service has changed our nickname
			set_our_groupchat_nick $chatid $nick
		    }
		}
	    }
	}
    }

    foreach child $children {
	::xmpp::xml::split $child tag xmlns attrs cdata subels
	switch -- $tag {
	    status {
		set code [::xmpp::xml::getAttr $attrs code]
		switch -- $code {
		    201 {
			chat::add_message \
			    $chatid $group groupchat \
			    [::msgcat::mc "A new room is created"] {}
			# 201: room creation
			if {$options(propose_configure)} {
			    request_config_dialog $chatid
			} else {
			    # requesting "instant" room as specified in XEP-0045
			    # if the user wants to configure room (s)he can do it later
			    request_instant_room $chatid
			}
		    }
		    301 -
		    307 -
		    321 -
		    322 {
			# 301: ban, 307: kick, 321: loosing membership
			# 322: room becomes members-only
			set real_jid [get_real_jid $xlib $group/$nick]
			if {$real_jid != ""} {
			    set real_jid " ($real_jid)"
			}
			switch -- $code {
			    301 {set action \
				     [::msgcat::mc "%s has been banned" \
					  $nick$real_jid]}
			    307 {set action \
				     [::msgcat::mc "%s has been kicked" \
					  $nick$real_jid]}
			    321 {set action \
				     [::msgcat::mc \
					  "%s has been kicked because\
					   of membership loss" \
					  $nick$real_jid]}
			    322 {set action \
				     [::msgcat::mc \
					  "%s has been kicked because\
					   room became members-only" \
					  $nick$real_jid]}
			}

			if {[info exists actor] && $actor != ""} {
			    append action [::msgcat::mc " by %s" $actor]
			}

			if {[info exists reason] && $reason != ""} {
			    append action ": $reason"
			}

			if {$options(gen_enter_exit_msgs)} {
			    variable ignore_unavailable $nick
			    chat::add_message \
				$chatid $group groupchat $action {}
			}
		    }
		    303 {
			# 303: nickname change
			if {[info exists new_nick] && $new_nick != ""} {
			    if {$nick == [get_our_groupchat_nick $chatid]} {
				set_our_groupchat_nick $chatid $new_nick
			    }
			    # TODO may be this reporting should not be done
			    # if the $nick is being ignored (MUC ignore)
			    if {$options(gen_enter_exit_msgs)} {
				variable ignore_available   $new_nick
				variable ignore_unavailable $nick

				set real_jid [get_real_jid $xlib $group/$nick]
				if {$real_jid != ""} {
				    set real_jid " ($real_jid)"
				}
				chat::add_message \
				    $chatid $group groupchat \
				    [::msgcat::mc "%s is now known as %s" \
					 $nick$real_jid $new_nick] {}
			    }
			    ::hook::run room_nickname_changed_hook \
					$xlib $group $nick $new_nick
			}
		    }
		}
	    }
	}
    }
}


proc muc::process_available {chatid nick} {
    variable pending_nicks

    set xlib [chat::get_xlib $chatid]
    set group [chat::get_jid $chatid]

    if {![is_compatible $group] && [is_changing_nick $chatid] && \
	    [string equal $pending_nicks($chatid) $nick]} {
	set_our_groupchat_nick $chatid $nick
	unset pending_nicks($chatid)
    }
}


proc muc::process_unavailable {chatid nick} {
    variable options
    variable ignore_unavailable

    set group [chat::get_jid $chatid]

    if {[is_compatible $group] && $options(gen_enter_exit_msgs) && \
	    (![info exists ignore_unavailable] || \
		 $ignore_unavailable != $nick)} {
	set xlib [chat::get_xlib $chatid]
	set error [get_jid_presence_info error $xlib $group/$nick]
	if {$error != ""} {
	    set status [::xmpp::stanzaerror::message $error]
	} else {
	    set status [get_jid_presence_info status $xlib $group/$nick]
	}
	if {$status != ""} {
	    set end ": $status"
	} else {
	    set end ""
	}
	chat::add_message $chatid $group groupchat \
	    [cconcat [::msgcat::mc "%s has left" $nick] $end] {}
    }
    catch { unset ignore_unavailable }
}

proc muc::report_available {chatid nick entered} {
    variable options
    variable users
    variable ignore_available

    set xlib [::chat::get_xlib $chatid]
    set group [::chat::get_jid $chatid]

    if {![is_compatible $group]} return

    set jid $group/$nick

    set msg ""

    set report_entered [expr {$entered && $options(gen_enter_exit_msgs) \
	&& (![info exists ignore_available] || $ignore_available != $nick)}]

    if {$report_entered} {
	set real_jid [get_real_jid $xlib $jid]
	if {$real_jid != ""} {
	    set occupant "$nick ($real_jid)"
	} else {
	    set occupant $nick
	}
	set msg [::msgcat::mc "%s has entered" $occupant]
	if {$options(gen_muc_position_change_msgs)} {
	    append msg " " [::msgcat::mc "as %s/%s" \
		[::msgcat::mc $users(affiliation,$xlib,$jid)] \
		[::msgcat::mc $users(role,$xlib,$jid)]]
	}
    }
    catch { unset ignore_available }

    if {$options(gen_muc_status_change_msgs)} {
	set status [::get_user_status $xlib $jid]
	if {$report_entered} {
	    append msg " " [::msgcat::mc "and"] " "
	} else {
	    append msg $nick " "
	}
	append msg [::get_long_status_desc $status]
	set desc [::get_user_status_desc $xlib $jid]
	if {$desc != {}} {
	    append msg " ($desc)"
	}
    }

    ::chat::add_message $chatid $group groupchat $msg {}
}

proc muc::track_room_position {xlib jid role affiliation} {
    variable options
    variable users

    upvar 0 users(role,$xlib,$jid) _role \
	users(affiliation,$xlib,$jid) _aff

    set group  [::xmpp::jid::stripResource $jid]
    if {![is_compatible $group]} return

    set chatid [chat::chatid $xlib $group]

    if {[chat::is_opened $chatid] && $options(gen_muc_position_change_msgs)} {
	set nick [chat::get_nick $xlib $jid groupchat]

	set role_changed [expr {![info exists _role] \
	    || ![string equal $role $_role]}]
	set aff_changed  [expr {![info exists _aff] \
	    || ![string equal $affiliation $_aff]}]

	if {$aff_changed} {
	    if {$role_changed} {
		set msg [::msgcat::mc "%s has been assigned a new room position: %s/%s" \
		    $nick [::msgcat::mc $affiliation] [::msgcat::mc $role]]
	    } else {
		set msg [::msgcat::mc "%s has been assigned a new affiliation: %s" \
		    $nick [::msgcat::mc $affiliation]]
	    }
	} elseif {$role_changed} {
	    set msg [::msgcat::mc "%s has been assigned a new role: %s" \
		$nick [::msgcat::mc $role]]
	} else {
	    set msg ""
	}

	if {$msg != ""} {
	    ::chat::add_message $chatid $group groupchat $msg {}
	}
    }

    set _aff  $affiliation
    set _role $role
}

###############################################################################

proc muc::change_nick {chatid nick} {
    global userstatus textstatus
    global statusdesc
    variable pids

    set group [chat::get_jid $chatid]
    set xlib [chat::get_xlib $chatid]

    if {![is_compatible $group]} {
	variable pending_nicks
	set pending_nicks($chatid) $nick
    }

    if {$userstatus == "invisible"} {
	set status available
    } else {
	set status $userstatus
    }

    set tstatus $textstatus

    set id [::xmpp::packetID $xlib]
    set pids($xlib,$id) $chatid

    send_custom_presence $xlib $group/$nick $status \
			 -status $tstatus \
			 -id $id
}


proc muc::process_change_nick {xlib from type x args} {
    variable pids

    set status ""
    set error [::msgcat::mc "Error"]
    foreach {key val} $args {
	switch -- $key {
	    -id     {set id $val}
	    -status {set status $val}
	    -error  {set error [::xmpp::stanzaerror::message $val]}
	}
    }

    if {![info exists id] || ![info exists pids($xlib,$id)]} {
	return
    }

    set chatid $pids($xlib,$id)
    unset pids($xlib,$id)

    if {![string equal $type error]} {
	return
    }

    if {![is_compatible [chat::get_jid $chatid]] && \
	    [is_changing_nick $chatid]} {
	variable pending_nicks
	unset pending_nicks($chatid)
    }

    chat::add_message $chatid [::xmpp::jid::stripResource $from] error $error {}
    return stop
}

hook::add client_presence_hook muc::process_change_nick 10

proc muc::is_changing_nick {chatid} {
    variable pending_nicks

    if {[info exists pending_nicks($chatid)]} {
	return 1
    } else {
	return 0
    }
}

proc muc::clear_changing_nicks {xlib} {
    variable pending_nicks
    variable pids

    array unset pending_nicks [list $xlib *]
    array unset pids $xlib,*
}

hook::add disconnected_hook muc::clear_changing_nicks

###############################################################################

proc muc::request_negotiation {xlib group} {
    variable muc_compatible

    # It's almost impossible to find MUC-incompatible room now, so the default
    # value is 1
    set muc_compatible($group) 1

    disco::request_info $xlib [::xmpp::jid::server $group] \
	-cache yes \
	-command [list muc::recv_negotiation1 $xlib $group]
}

proc muc::recv_negotiation1 {xlib group res identities features extras} {
    variable muc_compatible

    if {[string equal $res ok] && [lsearch -exact $features $::NS(muc)] >= 0} {
	set muc_compatible($group) 1
	set muc_compatible([::xmpp::jid::server $group]) 1
	return
    }

    disco::request_info $xlib $group \
	-cache yes \
	-command [list muc::recv_negotiation2 $group]
}

proc muc::recv_negotiation2 {group res identities features extras} {
    variable muc_compatible

    if {[string equal $res ok] && [lsearch -exact $features $::NS(muc)] >= 0} {
	set muc_compatible($group) 1
	return
    }

    set muc_compatible($group) 0
}

proc muc::is_compatible {group} {
    variable muc_compatible

    if {[info exists muc_compatible($group)]} {
	return $muc_compatible($group)
    } elseif {[info exists muc_compatible([::xmpp::jid::server $group])]} {
	return $muc_compatible([::xmpp::jid::server $group])
    } else {
	return 0
    }
}

###############################################################################

proc muc::add_user_popup_info {infovar xlib user} {
    variable users
    upvar 0 $infovar info

    if {[info exists users(jid,$xlib,$user)] && \
	    $users(jid,$xlib,$user) != ""} {
	append info [::msgcat::mc "\n\tJID: %s" $users(jid,$xlib,$user)]
    }
    if {[info exists users(affiliation,$xlib,$user)]} {
	append info [::msgcat::mc "\n\tAffiliation: %s" \
			 $users(affiliation,$xlib,$user)]
    }
}

hook::add roster_user_popup_info_hook muc::add_user_popup_info

###############################################################################

proc muc::set_message_timestamp {chatid from type body x} {
    variable timestamps

    if {![chat::is_disconnected $chatid]} {
	set timestamps($chatid) [clock seconds]
    }
}

hook::add draw_message_hook muc::set_message_timestamp 15

proc muc::clear_message_timestamp {chatid} {
    variable timestamps

    catch { unset timestamps($chatid) }
}

hook::add close_chat_post_hook muc::clear_message_timestamp

###############################################################################

proc muc::join_group {xlib group nick {password ""}} {
    global userstatus textstatus
    global statusdesc
    variable options
    variable timestamps
    variable muc_password
    variable joinIds

    set group [::xmpp::jid::normalize $group]
    set chatid [chat::chatid $xlib $group]

    if {[info exists chat::chats(status,$chatid)] && \
	    ![string equal $chat::chats(status,$chatid) disconnected]} {
	change_nick $chatid $nick
	return
    }

    privacy::add_to_special_list $xlib conference [::xmpp::jid::server $group]

    set_our_groupchat_nick $chatid $nick

    chat::open_window $chatid groupchat
    update idletasks

    request_negotiation $xlib $group

    set x_subtags {}

    lappend x_subtags [::xmpp::xml::create password -cdata $password]
    set muc_password($chatid) $password

    set history_vars {}
    if {$options(history_maxchars) >= 0} {
	lappend history_vars maxchars $options(history_maxchars)
    }
    if {$options(history_maxstanzas) >= 0} {
	lappend history_vars maxstanzas $options(history_maxstanzas)
    }
    if {$options(request_only_unseen_history) && \
	    [info exists timestamps($chatid)]} {
	lappend history_vars \
		seconds [expr {[clock seconds] - $timestamps($chatid) + 2}]
    }
    if {![lempty $history_vars]} {
	lappend x_subtags [::xmpp::xml::create history -attrs $history_vars]
    }

    if {$userstatus == "invisible"} {
	set status available
    } else {
	set status $userstatus
    }

    set tstatus $textstatus

    set id [::xmpp::packetID $xlib]
    set joinIds($xlib,$id) $chatid

    send_presence $xlib $status \
		  -to $group/$nick \
		  -status $tstatus \
		  -xlist [list [::xmpp::xml::create x \
				    -xmlns $::NS(muc) \
				    -subelements $x_subtags]] \
		  -id $id
}

proc muc::process_join {xlib from type x args} {
    variable joinIds

    set status ""
    set error [::msgcat::mc "Error"]
    foreach {key val} $args {
	switch -- $key {
	    -id     {set id $val}
	    -status {set status $val}
	    -error  {set error [::xmpp::stanzaerror::message $val]}
	}
    }

    if {![info exists id] || ![info exists joinIds($xlib,$id)]} {
	return
    }

    set chatid $joinIds($xlib,$id)
    unset joinIds($xlib,$id)

    if {![string equal $type error]} {
	return
    }

    chat::add_message $chatid [::xmpp::jid::stripResource $from] error $error {}
    return stop
}

hook::add client_presence_hook muc::process_join 10

###############################################################################

proc muc::leave_group {xlib group nick status} {
    send_presence $xlib unavailable \
		  -to $group/$nick \
		  -status $status
}

###############################################################################

proc muc::invite_muc {xlib group jid reason} {
    # If $jid is a 'real' JID then invite
    # If $jid is in a conference room try to invite real JID

    set real_jid [get_real_jid $xlib $jid]
    if {$real_jid == ""} {
	set real_jid $jid
    }

    message::send_msg $xlib $group \
	-xlist [list \
	    [::xmpp::xml::create x \
		 -xmlns $::NS(muc#user) \
		 -subelement [::xmpp::xml::create invite \
				    -attrs [list to $real_jid] \
				    -subelement [::xmpp::xml::create reason \
							-cdata $reason]]]]
}

proc muc::invite_xconference {xlib group jid reason} {
    message::send_msg $xlib $jid \
	-type normal \
	-subject "Invitation" \
	-body $reason \
	-xlist [list [::xmpp::xml::create x \
			    -xmlns $::NS(xconference) \
			    -attrs [list jid $group]]]
}

###############################################################################

proc muc::process_invitation {rowvar bodyvar f x xlib from id type replyP} {
    upvar 2 $rowvar row
    upvar 2 $bodyvar body

    foreach xa $x {
	::xmpp::xml::split $xa tag xmlns attrs cdata subels

	switch -- $xmlns \
	    $::NS(xconference) {
		set xconference_group [::xmpp::xml::getAttr $attrs jid]
		set xconference_password ""
		if {[string equal $body ""] && ![string equal $cdata ""]} {
		    set xconference_body $cdata
		} else {
		    set xconference_body $body
		}
	    } \
	    $::NS(muc#user) {
		set password ""
		set inviter ""
		foreach ch $subels {
		    ::xmpp::xml::split $ch stag sxmlns sattrs scdata ssubels

		    switch -- $stag {
			invite {
			    set inviter [::xmpp::xml::getAttr $sattrs from]
			    if {![string equal $inviter ""]} {
				foreach c [connections] {
				    set name \
					[roster::itemconfig $c \
					     [roster::find_jid $c $inviter] \
						  -name]
				    if {$name != ""} break
				}
				if {![cequal $name ""]} {
				    set inviter "$name ($inviter)"
				}
				set muc_body \
				    [::msgcat::mc \
					 "%s invites you to conference\
					  room %s" \
					 $inviter $from]

				foreach sch $ssubels {
				    ::xmpp::xml::split $sch sstag ssxmlns ssattrs \
							    sscdata sssubels
				    if {[string equal $sstag "reason"]} {
					append muc_body \
					    [::msgcat::mc "\nReason is: %s" \
						 $sscdata]
				    }
				}
			    }
			    # TODO decline
			}
			password {
			    set password $scdata
			}
		    }
		}
		if {![string equal $inviter ""]} {
		    set muc_group $from
		    set muc_password $password
		}
	    }
    }

    if {[info exists muc_group] && $muc_group != ""} {
	process_x_conference $f $xlib $muc_group $muc_password $row
	incr row
	set body $muc_body
	return
    } elseif {[info exists xconference_group] && $xconference_group != ""} {
	process_x_conference $f $xlib $xconference_group \
	    $xconference_password $row
	incr row
	set body $xconference_body
	return
    }
    
    return
}

hook::add message_process_x_hook muc::process_invitation

proc muc::process_x_conference {f xlib group password row} {
    global gr_nick

    label $f.lgroup$row -text [::msgcat::mc "Invited to:"]
    button $f.group$row -text $group \
	-command [list ::join_group $xlib $group \
		       -nick [get_group_nick $group $gr_nick] \
		       -password $password]
    
    grid $f.lgroup$row -row $row -column 0 -sticky e
    grid $f.group$row  -row $row -column 1 -sticky ew
}

###############################################################################

proc muc::join {xlib jid args} {
    global gr_nick

    set category conference
    foreach {opt val} $args {
	switch -- $opt {
	    -category { set category $val }
	}
    }

    if {![cequal $category conference]} {
	return
    }

    if {![cequal [::xmpp::jid::node $jid] {}]} {
	::join_group $xlib $jid -nick [get_group_nick $jid $gr_nick]
    } else {
	::join_group_dialog $xlib -server [::xmpp::jid::server $jid] -group {}
    }
}

hook::add postload_hook \
    [list disco::browser::register_feature_handler jabber:iq:conference \
	 muc::join -desc [list conference [::msgcat::mc "Join conference"]]]
hook::add postload_hook \
    [list disco::browser::register_feature_handler $::NS(muc) muc::join \
	 -desc [list conference [::msgcat::mc "Join conference"]]]
hook::add postload_hook \
    [list disco::browser::register_feature_handler "gc-1.0" muc::join \
	 -desc [list conference [::msgcat::mc "Join groupchat"]]]

###############################################################################


proc muc::disco_reply {type xlib from lang} {
    variable options

    if {!$options(report_muc_rooms)} {
	return {error cancel not-allowed}
    }

    switch -- $type {
	info {
	    return [list result {}]
	}
	items {
	    set res {}
	    foreach chatid [lfilter chat::is_groupchat [chat::opened $xlib]] {
		set group [chat::get_jid $chatid]
		if {[is_compatible $group]} {
		    lappend res [list jid $group]
		}
	    }
	    return [list result $res]
	}
    }
}

hook::add postload_hook \
    [list disco::register_node $::NS(muc#rooms) muc::disco_reply \
	  [::trans::trans "Current rooms"]]

###############################################################################

# vim:ts=8:sw=4:sts=4:noet
