1# $Id$ 2# Support for ignoring occupant activity in MUC rooms. 3# 4# A note on runtime ruleset format: 5# * A hash is used to hold ignore rules at runtime; each key 6# uniquely refers to its related "xlib, room, occupant, 7# message type" tuple; the existence of a key is used to 8# determine the fact of some type of a room occupant messages 9# being ignored. 10# * The format of the ruleset keys is as follows: 11# SESSION_JID NUL ROOM_JID/OCCUPANT NUL TYPE 12# where: 13# * NUL means character with code 0 (\u0000 in Tcl lingo). 14# It is used since ASCII NUL is prohibited in JIDs; 15# * SESSION_JID is the bare JID of a particular connection of 16# the Tkabber user (support for multiaccounting); 17# * ROOM_JID is the room bare JID; 18# * OCCUPANT is either an occupant's room nick OR her full 19# bare JID, if it's available; 20# * TYPE is either "chat" or "groupchat", literally, which determines 21# the type of messages to ignore. 22 23namespace eval mucignore { 24 variable options 25 26 variable ignored 27 28 variable tid 0 29 variable tags 30 31 variable menustate 32 33 # Cutsomize section: 34 35 custom::defvar stored_rules {} \ 36 "Stored MUC ignore rules" \ 37 -group Hidden \ 38 -type string 39 40 hook::add post_custom_restore [namespace current]::restore_rules 41 42 custom::defgroup {MUC Ignoring} \ 43 [::msgcat::mc "Ignoring groupchat and chat messages\ 44 from selected occupants of multi-user conference\ 45 rooms."] \ 46 -group Privacy \ 47 -group Chat 48 49 custom::defvar options(transient_rules) 0 \ 50 [::msgcat::mc "When set, all changes to the ignore rules are\ 51 applied only until Tkabber is closed\;\ 52 they are not saved and thus will be not restored at\ 53 the next run."] \ 54 -group {MUC Ignoring} \ 55 -type boolean 56 57 # Event handlers: 58 59 # Handlers for creating various menus: 60 hook::add chat_create_conference_menu_hook \ 61 [namespace current]::setup_muc_menu 62 hook::add chat_create_user_menu_hook \ 63 [namespace current]::setup_private_muc_chat_menu 64 hook::add roster_create_groupchat_user_menu_hook \ 65 [namespace current]::setup_occupant_menu 66 hook::add finload_hook \ 67 [namespace current]::on_init 68 69 # Block private MUC messages: 70 hook::add process_message_hook \ 71 [namespace current]::process_message 72 73 # Weed out MUC room messages upon entering a room: 74 hook::add open_chat_post_hook \ 75 [namespace current]::sanitize_muc_display 76 77 # Catch presence of ignored users. 78 # NOTE: the order of this handler must be higher than 79 # that of ::muc::process_presence (which is the default of 50) 80 # since that handler extracts and stores the room occupant's 81 # real JID in the non-anonymous rooms. 82 hook::add client_presence_hook \ 83 [namespace current]::catch_junkie_presence 55 84 85 # Adjust ignore rules on nick renames. 86 # NOTE: this hook must be run earlier than client_presence_hook. 87 hook::add room_nickname_changed_hook \ 88 [namespace current]::trace_room_nick_change 89 90 hook::add generate_completions_hook \ 91 [namespace current]::add_completions 92 93 hook::add chat_send_message_hook \ 94 [namespace current]::handle_commands 95} 96 97# "Ignore tags" are used to mark whole messages posted in the room 98# by an ignored occupant. Their names are autogenerated and unique 99# throughout one Tkabber run. Each tag is bound to one particular 100# "room JID" of the ignored occupant. Ignore tags may be rebound 101# to another room JID when these change (on nickname changes). 102 103# Creates the ignore tag for a particular "room JID". 104# If matching tag exists, this proc does nothing, silently. 105# This provides for ignore tag "persistence". 106proc mucignore::ignore_tag_create {roomjid} { 107 variable tid 108 variable tags 109 110 if {[info exists tags($roomjid)]} return 111 112 set tags($roomjid) IGNORED-$tid 113 incr tid 114} 115 116proc mucignore::ignore_tag_get {roomjid} { 117 variable tags 118 119 set tags($roomjid) 120} 121 122proc mucignore::ignore_tag_rebind {from to} { 123 variable tags 124 125 set tags($to) $tags($from) 126 unset tags($from) 127} 128 129proc mucignore::ignore_tag_forget {roomjid} { 130 variable tags 131 132 unset tags($roomjid) 133} 134 135# Returns bare JID of the session identified by $xlib 136proc mucignore::session_bare_jid {xlib} { 137 ::::xmpp::jid::stripResource [connection_jid $xlib] 138} 139 140# Tries to get the real bare JID of the room occupant identified 141# by the $room_occupant_jid; returns that JID if it's available, 142# empty string otherwise. 143proc mucignore::get_real_bare_jid {xlib room_occupant_jid} { 144 set real_jid [::muc::get_real_jid $xlib $room_occupant_jid] 145 if {$real_jid != {}} { 146 return [::::xmpp::jid::stripResource $real_jid] 147 } else { 148 return {} 149 } 150} 151 152# Creates an ignore rule suitable for using as a key to a hash of rules. 153# Expects: 154# * entity -- session's bare JID; 155# * jid -- JID to ignore ("room/nick" or "room/real_bare_jid"); 156# * type -- type of chat to ignore ("groupchat" or "chat"). 157# These parts are joined using the NUL character (since its appearance 158# is prohibited in any part of a JID) and so the rule can be reliably 159# split back into parts. 160# See also: [split_rule]. 161proc mucignore::mkrulekey {entity jid type} { 162 join [list $entity $jid $type] \u0000 163} 164 165# Creates an ignore rule suitable for using as a key to a hash of rules. 166# The $xlib parameter is converted to the session's bare JID first. 167# It's just a convenient wrapper around [mkrulekey]. 168proc mucignore::mkrule {xlib jid type} { 169 mkrulekey [session_bare_jid $xlib] $jid $type 170} 171 172# Splits given rule into the list of [entity jid type], where: 173# * entity -- is a bare JID of the user's session; 174# * jid -- is a JID to be ignored (usually a full room JID); 175# * type -- one of: "groupchat" or "chat", designating the type of messages 176# originating from jid to be ignored. 177# This proc reverses what [mkrulekey] does. 178proc mucignore::split_rule {rule} { 179 split $rule \u0000 180} 181 182proc mucignore::setup_muc_menu {m xlib jid} { 183 # TODO 184 return 185 $m add command \ 186 -label [::msgcat::mc "Edit MUC ignore rules"] \ 187 -command [list [namespace current]::editor::open $xlib $jid] 188} 189 190proc mucignore::on_init {} { 191 # TODO 192 return 193 set menu [.mainframe getmenu plugins] 194 $menu add command -label [::msgcat::mc "Edit MUC ignore rules"] \ 195 -command [list [namespace current]::editor::open {} {}] 196} 197 198proc mucignore::setup_private_muc_chat_menu {m xlib jid} { 199 set room [::::xmpp::jid::stripResource $jid] 200 if {![::chat::is_groupchat [::chat::chatid $xlib $room]]} return 201 202 setup_occupant_menu $m $xlib $jid 203} 204 205# Prepares two global variables mirroring the current state of 206# ignoring for the room occupant on which groupchat roster nick 207# the menu is being created. They are used to represent 208# ignore state checkbutton menu entries. 209proc mucignore::setup_occupant_menu {m xlib jid} { 210 variable ignored 211 variable menustate 212 213 set our_nick [::get_our_groupchat_nick [ 214 ::chat::chatid $xlib [ 215 ::::xmpp::jid::stripResource $jid]]] 216 set nick [::chat::get_nick $xlib $jid groupchat] 217 218 if {$nick == $our_nick} { 219 # don't allow to ignore ourselves 220 set state disabled 221 } else { 222 set state normal 223 } 224 225 foreach type {groupchat chat} { 226 set menustate($xlib,$jid,$type) [ 227 info exists ignored([mkrule $xlib $jid $type])] 228 } 229 230 set sm [menu $m.mucignore -tearoff 0] 231 $m add cascade -menu $sm \ 232 -state $state \ 233 -label [::msgcat::mc "Ignore"] 234 235 $sm add checkbutton -label [::msgcat::mc "Ignore groupchat messages"] \ 236 -variable [namespace current]::menustate($xlib,$jid,groupchat) \ 237 -command [list [namespace current]::menu_toggle_ignoring \ 238 $xlib $jid groupchat] 239 $sm add checkbutton -label [::msgcat::mc "Ignore chat messages"] \ 240 -variable [namespace current]::menustate($xlib,$jid,chat) \ 241 -command [list [namespace current]::menu_toggle_ignoring \ 242 $xlib $jid chat] 243 244 bind $m <Destroy> +[double% [list \ 245 [namespace current]::menu_cleanup_state $xlib $jid]] 246} 247 248proc mucignore::menu_toggle_ignoring {xlib jid type} { 249 variable menustate 250 251 if {$menustate($xlib,$jid,$type)} { 252 occupant_ignore $xlib $jid $type 253 } else { 254 occupant_attend $xlib $jid $type 255 } 256} 257 258proc mucignore::menu_cleanup_state {xlib jid} { 259 variable menustate 260 261 array unset menustate $xlib,$jid,* 262} 263 264# Ignores specified room occupant: 265# * Creates an ignore rule for her; 266# * Creates an ignore tag, if needed; 267# * Hides messages tagged with that tag, if any; 268# * Builds and saves current ruleset to the Customize db. 269proc mucignore::occupant_ignore {xlib jid args} { 270 variable options 271 variable ignored 272 273 foreach type $args { 274 set ignored([mkrule $xlib $jid $type]) true 275 276 if {$type == "groupchat"} { 277 ignore_tag_create $jid 278 room_weed_messages $xlib $jid true 279 } 280 } 281 282 if {!$options(transient_rules)} { 283 store_rules $xlib 284 } 285} 286 287# Un-ignores specified room occupant: 288# * Removes her ignore rules; 289# * Shows any hidden messages from her; 290# * Ignore tag is NOT removed to provide for "quick picking" 291# into what the ignored occupant have had written so far -- 292# when she is ignored again, all her messages tagged with 293# the appropriate ignore tag are again hidden. 294# * Builds and saves current ruleset to the Customize db. 295proc mucignore::occupant_attend {xlib jid args} { 296 variable options 297 variable ignored 298 299 foreach type $args { 300 set rule [mkrule $xlib $jid $type] 301 if {[info exists ignored($rule)]} { 302 unset ignored($rule) 303 if {$type == "groupchat"} { 304 room_weed_messages $xlib $jid false 305 # we don't use [ignore_tag_forget] here 306 # so when we switch ignoring back on, 307 # all already marked messagess will be weed out 308 } 309 } 310 } 311 312 if {!$options(transient_rules)} { 313 store_rules $xlib 314 } 315} 316 317# Hides or shows messages tagged as ignored for the $jid, if any. 318proc mucignore::room_weed_messages {xlib jid hide} { 319 set room [::::xmpp::jid::stripResource $jid] 320 set cw [::chat::chat_win [::chat::chatid $xlib $room]] 321 322 $cw tag configure [ignore_tag_get $jid] -elide $hide 323} 324 325# This handler blocks further processing of the private room message 326# if its sender is blacklisted. 327# If the message is groupchat and its sender is blacklisted, it sets 328# the appropriate message property so that other message handlers 329# could treat such message in some special way. 330proc mucignore::process_message {xlib from id type args} { 331 variable ignored 332 333 if {$type == "chat" && \ 334 [info exists ignored([mkrule $xlib $from chat])]} { 335 return stop 336 } 337} 338 339proc mucignore::is_ignored {xlib jid type} { 340 variable ignored 341 342 if {[info exists ignored([mkrule $xlib $jid $type])]} { 343 return [ignore_tag_get $jid] 344 } else { 345 return "" 346 } 347} 348 349# This handler is being run after opening the chat window. 350# It searches the ignore rules for JIDs matching the JID of the room, 351# extracts them from the rules and weeds out their messages from 352# the room display (chatlog). 353# NOTE that it gets executed before any presences arrive from the room 354# occupants, so the whole idea is to weed out messages with known (ignored) 355# nicks. 356proc mucignore::sanitize_muc_display {chatid type} { 357 variable ignored 358 359 if {$type != "groupchat"} return 360 361 set xlib [::chat::get_xlib $chatid] 362 set jid [::chat::get_jid $chatid] 363 364 foreach rule [array names ignored [mkrule $xlib $jid/* groupchat]] { 365 set junkie [lindex [split_rule $rule] 1] 366 # TODO handle "real JIDs" case... 367 ignore_tag_create $junkie 368 room_weed_messages $xlib $junkie true 369 } 370} 371 372# This handler is being run after the room_nickname_changed_hook 373# (which takes care of renaming the ignore list entries). 374# This proc serves two purposes: 375# * It converts rules from real JIDs and room JIDs and back 376# so that room JIDs are used for rule matching and real JIDs 377# are stored, if they are available, between sessions. 378# * It arranges for chat log display to be prepared to weed out 379# messages from ignored JIDs. 380 381# TODO why does real JID is available when this handler is run with 382# $type == "unavailable". memory leak in chats.tcl? 383# TODO use chat_user_enter/chat_user_exit instead? 384proc mucignore::catch_junkie_presence {xlib from pres args} { 385 variable options 386 variable ignored 387 388 set room [::::xmpp::jid::stripResource $from] 389 set rjid [get_real_bare_jid $xlib $from] 390 391 if {$pres == "available"} { 392 debugmsg mucignore "avail: $from; real jid: $rjid" 393 foreach type {groupchat chat} { 394 if {$rjid != {} && \ 395 [info exists ignored([mkrule $xlib $room/$rjid $type])]} { 396 rename_rule_jid $xlib $room/$rjid $from $type 397 } 398 } 399 400 if {[info exists ignored([mkrule $xlib $from groupchat])]} { 401 ignore_tag_create $from 402 room_weed_messages $xlib $from true 403 } 404 } elseif {$pres == "unavailable"} { 405 debugmsg mucignore "unavail: $from; real jid: $rjid" 406 if {[info exists ignored([mkrule $xlib $from groupchat])]} { 407 ignore_tag_forget $from 408 } 409 410 foreach type {groupchat chat} { 411 if {$rjid != {} && \ 412 [info exists ignored([mkrule $xlib $from $type])]} { 413 rename_rule_jid $xlib $from $room/$rjid $type 414 } 415 } 416 } 417} 418 419proc mucignore::trace_room_nick_change {chatid oldnick newnick} { 420 variable ignored 421 422 set xlib [chat::get_xlib $chatid] 423 set room [chat::get_jid $chatid] 424 foreach type {groupchat chat} { 425 if {[info exists ignored([mkrule $xlib $room/$oldnick $type])]} { 426 rename_rule_jid $xlib $room/$oldnick $room/$newnick $type 427 428 if {$type == "groupchat"} { 429 ignore_tag_rebind $room/$oldnick $room/$newnick 430 } 431 } 432 } 433} 434 435proc mucignore::rename_rule_jid {xlib from to type} { 436 variable ignored 437 438 set oldrule [mkrule $xlib $from $type] 439 set newrule [mkrule $xlib $to $type] 440 441 set ignored($newrule) [set ignored($oldrule)] 442 unset ignored($oldrule) 443 444 debugmsg mucignore "rule renamed:\ 445 [string map {\u0000 |} $oldrule]\ 446 [string map {\u0000 |} $newrule]" 447} 448 449proc mucignore::explode_room_jid {xlib room_occupant_jid vroom voccupant} { 450 upvar 1 $vroom room $voccupant occupant 451 452 set room [::::xmpp::jid::stripResource $room_occupant_jid] 453 454 set occupant [get_real_bare_jid $xlib $room_occupant_jid] 455 if {$occupant == {}} { 456 set occupant [::::xmpp::jid::resource $room_occupant_jid] 457 } 458} 459 460# Parses the runtime hash of ignore rules, makes up the hierarchical list 461# (a tree) of ignore rules, resolving the room JIDs to real JIDs, 462# if possible, then saves the list to the corresponding Customize variable. 463# The list has the form: 464# * session_bare_jid_1 465# * room_bare_jid_1 466# * occupant_1 (nick or real_jid) 467# * "groupchat" or "chat" or both 468# ...and so on 469proc mucignore::store_rules {xlib} { 470 variable ignored 471 variable stored_rules 472 473 array set entities {} 474 475 foreach rule [array names ignored] { 476 lassign [split_rule $rule] entity jid type 477 478 explode_room_jid $xlib $jid room occupant 479 480 set entities($entity) 1 481 482 set rooms rooms_$entity 483 if {![info exists $rooms]} { 484 array set $rooms {} 485 } 486 set [set rooms]($room) 1 487 488 set occupants occupants_$entity$room 489 if {![info exists $occupants]} { 490 array set $occupants {} 491 } 492 493 lappend [set occupants]($occupant) $type 494 } 495 496 set LE {} 497 foreach entity [array names entities] { 498 set LR {} 499 foreach room [array names rooms_$entity] { 500 set LO {} 501 set occupants occupants_$entity$room 502 foreach occupant [array names $occupants] { 503 lappend LO $occupant [set [set occupants]($occupant)] 504 } 505 506 lappend LR $room $LO 507 } 508 509 lappend LE $entity $LR 510 } 511 512 set stored_rules [list 1.0 $LE] ;# also record "ruleset syntax" version 513 514 debugmsg mucignore "STORED: $LE" 515} 516 517proc mucignore::restore_rules {args} { 518 variable ignored 519 variable stored_rules 520 521 array set ignored {} 522 523 set failed [catch { 524 lassign $stored_rules version ruleset 525 array set entities $ruleset 526 foreach entity [array names entities] { 527 array set rooms $entities($entity) 528 foreach room [array names rooms] { 529 array set occupants $rooms($room) 530 foreach occupant [array names occupants] { 531 foreach type $occupants($occupant) { 532 set ignored([mkrulekey $entity $room/$occupant $type]) true 533 } 534 } 535 array unset occupants 536 } 537 array unset rooms 538 } 539 } err] 540 541 if {$failed} { 542 global errorInfo 543 set bt $errorInfo 544 545 set stored_rules {} 546 547 after idle [list error \ 548 [::msgcat::mc "Error loading MUC ignore rules, purged."] $bt] 549 } 550 551 debugmsg mucignore "RESTORED: [string map {\u0000 |} [array names ignored]]" 552} 553 554proc mucignore::add_completions {chatid compsvar wordstart line} { 555 upvar 0 $compsvar comps 556 lappend comps "/ignore " "/unignore " 557} 558 559proc mucignore::handle_commands {chatid user body type} { 560 set xlib [chat::get_xlib $chatid] 561 562 if {[string equal -length 8 $body "/ignore "]} { 563 set nick [string range $body 8 end] 564 set jid [chat::get_jid $chatid]/$nick 565 occupant_ignore $xlib $jid $type 566 567 set msg [::msgcat::mc "%s was ignored" $nick] 568 ::chat::add_message $chatid "" chat $msg {} 569 570 return stop 571 } 572 if {[string equal -length 10 $body "/unignore "]} { 573 set nick [string range $body 10 end] 574 set jid [chat::get_jid $chatid]/$nick 575 occupant_attend $xlib $jid $type 576 577 set msg [::msgcat::mc "%s was unignored" $nick] 578 ::chat::add_message $chatid "" chat $msg {} 579 580 return stop 581 } 582} 583 584######################################################################## 585# MUC Ignore ruleset editor 586######################################################################## 587 588namespace eval mucignore::editor {} 589 590# ... 591# NOTE that both $xlib and $jid may be empty at the time of invocation. 592proc mucignore::editor::open {xlib jid} { 593 set w .mucignore_rules_editor 594 if {[winfo exists $w]} { 595 return 596 } 597 598 add_win $w -title [::msgcat::mc "MUC Ignore Rules"] \ 599 -tabtitle [::msgcat::mc "MUC Ignore"] \ 600 -class MUCIgnoreRulesetEditor \ 601 -raise 1 602 #-raisecmd "focus [list $w.input]" 603 604 set sw [ScrolledWindow $w.sw -auto both] 605 set t [Tree $w.tree -background [$w cget -background]] 606 $sw setwidget $t 607 608 bind $sw <Destroy> [list [namespace current]::cleanup [double% $w]] 609 610 pack $sw -fill both -expand true 611 612 # NOTE that BWidget Tree doesn't aceept keyboard bindings. 613 614 $t bindText <Double-ButtonPress-1> [list %W toggle] 615 bind $w <KeyPress-Return> [list [namespace current]::tree_toggle [double% $t]] 616 617 bind $w <KeyPress-F2> [list [namespace current]::tree_edit_item [double% $t]] 618 bind $w <Any-KeyPress-Insert> [list [namespace current]::tree_insert_item [double% $t]] 619 bind $w <Any-KeyPress-Delete> [list [namespace current]::tree_insert_item [double% $t]] 620} 621 622proc mucignore::editor::cleanup {w} { 623 # TODO do appropriate cleanup... 624} 625 626proc mucignore::editor::tree_toggle {t} { 627 set node [lindex [$t selection get] 0] 628 if {$node != {}} { 629 $t toggle $node 630 } 631} 632 633proc mucignore::editor::tree_edit_item {t} { 634 set node [lindex [$t selection get] 0] 635 if {$node == {}} return 636 637 set text [$t itemcget $node -text] 638 639 $t edit $node $text 640} 641 642proc mucignore::editor::tree_insert_item {t} { 643 set parent [lindex [$t selection get] 0] 644 645 if {$parent == {}} { 646 set parent root 647 } 648 649 # TODO implement 650 #add_nodes $t $parent {New {}} 651} 652 653# vim:ts=8:sw=4:sts=4:noet 654