weechat-scripts/0000755000175100017510000000000015112622421012643 5ustar manumanuweechat-scripts/tcl/0000755000175100017510000000000015112622420013424 5ustar manumanuweechat-scripts/tcl/wttr.tcl0000644000175100017510000001132415112622420015131 0ustar manumanu# Copyright (c) 2023 by CrazyCat # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # --------------------------------------------- # Adds an item showing weather # # --------------------------------------------- # History # 2023-06-15 : Improved help # 2023-06-14 : Initial release set SCRIPT_VERSION 1.1 set SCRIPT_NAME wttr set SCRIPT_SUMMARY "Adds an item showing weather" set SCRIPT_ARGS "loc |format <1-4|format>|lang " set SCRIPT_ADESC "loc : sets the new location\n\ : example : /wttr loc Paris, France\n\ * format : Formats of the output, can be an integer (predefined formats from 1 to 4) or a string (custom format).\n\ : example : /wttr format %l:+%C+%t+(%f)+%w\n\ -- Main format variables --\n\ : %l : location\n\ : %c / %C / %x: weather condition (icon / textual)\n\ : %t / %f : temperature (actual / feels like)\n\ : %w : wind\n\ -- Predefined formats --\n\ : 1 - %c+%t\n\ : 2 - %c+%t+%w (with icons)\n\ : 3 - %l:+%c+%t\n\ : 4 - %l:%c+%t+%w (with icons)\n\ : More explanation @ https://github.com/chubin/wttr.in#one-line-output\n\ * lang : Defines the lang to use (EN for english, FR for french, ...). Default is your weechat lang.\n\ Think to add the \[wttr\] item to a bar. Example to add it to the status bar:\n\ /eval /set weechat.bar.status.items \"\${weechat.bar.status.items},wttr\")" weechat::register $SCRIPT_NAME {CrazyCat } $SCRIPT_VERSION GPL3 $SCRIPT_SUMMARY {} {} weechat::hook_command wttr $SCRIPT_SUMMARY $SCRIPT_ARGS $SCRIPT_ADESC {loc || format || lang} wttr_cmds {} # Management of settings proc wttr_cmds {data buffer args} { set value [lassign {*}$args cmd] if {$cmd eq "" || [string tolower $cmd] eq "help"} { weechat::command "" "/help wttr" return $::weechat::WEECHAT_RC_OK } set cmd [string tolower $cmd] switch -nocase $cmd { "loc" { if {$value eq ""} { weechat::print $buffer "Use /wttr set loc City" return $weechat::WEECHAT_RC_ERROR } weechat::config_set_plugin city [join $value] } "format" { if {$value eq ""} { weechat::print $buffer "Using default format" set value 4 } weechat::config_set_plugin wformat [join $value] } "lang" { if {$value eq ""} { weechat::print $buffer "Using weechat locale" set value [lindex [split [::weechat::info_get "locale" ""] "_"] 0] } weechat::config_set_plugin lang $value } default { weechat::print $buffer "Usage : /wttr value" return $::weechat::WEECHAT_RC_ERROR } } wttr_timer_cb "" 0 return $::weechat::WEECHAT_RC_OK } # Periodical call proc wttr_timer_cb {data remaining_calls} { set city [string map {" " "%20"} [weechat::config_get_plugin city]] set url "http://wttr.in/$city?format=[weechat::config_get_plugin wformat]&lang=[weechat::config_get_plugin lang]" weechat::hook_process "url:${url}" 5000 "wttr_get_cb" "" return $::weechat::WEECHAT_RC_OK } # Callback when getting datas from wttr.in proc wttr_get_cb { data command rc out err} { global wttr_value if {$out ne ""} { set wttr_value $out weechat::bar_item_update "wttr" } return $::weechat::WEECHAT_RC_OK } # Update of the item proc wttr_show {args} { global wttr_value if {[info exists wttr_value] && $wttr_value ne ""} { return $wttr_value } return "[weechat::config_get_plugin city] : no data" } # Initial settings if {[set city [weechat::config_get_plugin city]] eq ""} { weechat::config_set_plugin city "Paris" } if {[set wformat [weechat::config_get_plugin wformat]] eq ""} { weechat::config_set_plugin wformat 4 } if {[set refresh [weechat::config_get_plugin refresh]] eq ""} { weechat::config_set_plugin refresh 300 } if {[set refresh [weechat::config_get_plugin lang]] eq ""} { set tlang [split [::weechat::info_get "locale" ""] "_"] weechat::config_set_plugin lang [lindex $tlang 0] } weechat::hook_timer [expr [weechat::config_get_plugin refresh]*1000] 60 0 wttr_timer_cb "" weechat::bar_item_new "wttr" "wttr_show" "" weechat-scripts/tcl/rnotify.tcl0000644000175100017510000001052015112622420015620 0ustar manumanu# Remote Notification Script v1.4 # by Gotisch # # With help of this script you can make weechat create notification bubbles # in ubuntu or any other distribution that supports libnotify. # # Changelog: # 1.4 # fixed problem with reserved characters preventing notification (see http://wiki.tcl.tk/1353) (thanks Ongy) # 1.3 # fixed yet more typos and a possible problem with notifications not showing when they should. # 1.2 # fixed small typo that prevented remote notification (thanks Jesse) # 1.1 # added setting: privmessage to customize notifications of messages in query # 1.0 # initial release # # How does it work? # # The script inside weechat will either call libnotify directly, or it will # send the data to the "server" listening on a port which will call the # libnotify executable and create the notification. This "remote" option # is the main use of the script. # # Example 1: Weechat runs on the local pc # /tcl load rnotify.tcl # and set the port # /set plugins.var.tcl.rnotify.port local # # Example 2: Weechat runs on a remote pc and you login via ssh port you # want to use is 4321 # sh location/of/rnotify.tcl 4321 & ssh -R 4321:localhost:4321 username@host # on server you start weechat (or resume screen or whatever). # Then inside weechat # /tcl load rnotify.tcl # and set the port # /set plugins.var.tcl.rnotify.port 4321 # # General Syntax: # In weechat # /set plugins.var.tcl.rnotify.port # To get notifications for private messages set: # /set plugins.var.tcl.rnotify.privmessage [no(default)|all|inactive] # no - no notifications for private messages (besides on highlight) # all - notifications for all private messages # inactive - notifications only for messages that are not the currently active buffer # As script # rnotify.tcl # if no port is given it will listen on 1234. # # Requirements: # libnotify (esp. notify-send executable) # # Possible problems: # It could be other programs send data to the notify port when using remote # mode. This will then lead to the following: either break the script, or # make weird notification bubbles. # \ exec tclsh "$0" ${1+"$@"} if {[namespace exists ::weechat]} { # We have been called inside weechat namespace eval weechat::script::rnotify { weechat::register "rnotify" {Gotisch gotisch@gmail.com} 1.4 GPL3 {Sends highlights to (remote) client} {} {} proc highlight { data buffer date tags displayed highlight prefix message } { set buffername [weechat::buffer_get_string $buffer short_name] if {$buffername != $prefix} { set buffername "$prefix in $buffername" if {$highlight == 0} { return $::weechat::WEECHAT_RC_OK } } else { if {![string equal -nocase [weechat::config_get_plugin privmessage] "all"]} { if {![string equal -nocase [weechat::config_get_plugin privmessage] "inactive"] || [string equal [weechat::buffer_get_string [weechat::current_buffer] short_name] $buffername]} { if {$highlight == 0} { return $::weechat::WEECHAT_RC_OK } } } set buffername "$prefix in query" } notify $buffername $message return $::weechat::WEECHAT_RC_OK } proc notify {title text} { set title "\u200B$title" set text "\u200B$text" if {[weechat::config_get_plugin port] == "local"} { catch { exec notify-send -u normal -c IRC -i gtk-help "$title" "$text" } } else { catch { set sock [socket -async localhost [weechat::config_get_plugin port]] puts $sock [list normal gtk-help $title $text] close $sock } } } weechat::hook_print "" "irc_privmsg" "" 1 [namespace current]::highlight {} } } else { # We probably have been called from the shell set port 1234 if {[llength $argv] == 1} { set port $argv } proc notify_server {port} { set s [socket -server accept $port] puts "Listening on $port for Connections..." vwait forever } proc accept {sock addr port} { fileevent $sock readable [list recieve $sock] } proc recieve {sock} { if {[eof $sock] || [catch {gets $sock line}]} { close $sock } else { foreach {urgency icon title text} $line { exec notify-send -u $urgency -c IRC -i $icon "$title" "$text" } } } notify_server $port } weechat-scripts/tcl/inverter.tcl0000644000175100017510000000270315112622420015770 0ustar manumanu# Copyright (c) 2016 by CrazyCat # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Invert all letters of your text # # Usage: /inv Your text here # # Output: ereh txet ruoY set VERSION 0.1 set SCRIPT_NAME inverter weechat::register $SCRIPT_NAME {CrazyCat } $VERSION GPL3 {invert all letters of your text} {} {} weechat::hook_command inv {invert all letters of the text} {} {} {} invert_cmd {} proc invert_cmd {data buffer args} { set text [join $args] set channel [weechat::buffer_get_string $buffer localvar_channel] if { $text eq "" } { weechat::print $buffer "You need a text to revert" return $::weechat::WEECHAT_RC_ERROR } set inv "" for {set i 0} {$i<=[string length $text]} {incr i} { append inv [string index $text end-$i] } weechat::command $buffer "/msg $channel $inv" return $::weechat::WEECHAT_RC_OK } weechat-scripts/tcl/chan_hl.tcl0000644000175100017510000001212115112622420015521 0ustar manumanu# Copyright (c) 2009 by Dmitry Kobylin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # mark channels to highlight on each message # # this script is useful with other scripts like: # beep.pl, welauncher.pl, notify.py etc # # to mark channels on strartup set option "default_list" of this # script ( via iset.pl plugin (/iset command) or /setp command) # # by default if message appeared on current channel ( and channel # is marked) highlight event is not sended, to prevent this behaviour # set "hl_on_cur_chan" option to 1 # # Usage: # /mark # /smark # /unmark # /unmark all # /set plugins.var.tcl.chan_hl.default_list "#chan1,#chan2,#somechan" # /set plugins.var.tcl.chan_hl.hl_on_cur_chan 0 # # TODO: # * separate channels with same name on different servers # # 2009-05-02, FlashCode : # version 0.2: sync with last API changes # 2009-04-17, Dmitry Kobylin : # version 0.1 # set VERSION 0.2 set SCRIPT_NAME chan_hl weechat::register \ $SCRIPT_NAME {Karvur } $VERSION GPL3 \ {mark channels to highlight on each message} {} {} set MARK_LIST [list] if {[set DEFAULT_LIST [weechat::config_get_plugin default_list]] eq ""} { weechat::config_set_plugin default_list "" } else { foreach element [split $DEFAULT_LIST ,] {lappend MARK_LIST $element} } if {[set HL_ON_CUR_CHAN [weechat::config_get_plugin hl_on_cur_chan]] eq ""} { weechat::config_set_plugin hl_on_cur_chan 0 set HL_ON_CUR_CHAN 0 } proc config_changed {data option value} { set ::HL_ON_CUR_CHAN $value return $::weechat::WEECHAT_CONFIG_OPTION_SET_OK_CHANGED } proc mark_cmd {data buffer args} { set channel [weechat::buffer_get_string $buffer localvar_channel] if {[weechat::info_get irc_is_channel $channel] eq "1"} { if {[lsearch $::MARK_LIST $channel] == -1} { lappend ::MARK_LIST $channel weechat::print $buffer "channel \"$channel\" was appended to notify list" } else { weechat::print $buffer "channel \"$channel\" already in notify list" } } else { weechat::print $buffer "this command must be executed on channel" } return $::weechat::WEECHAT_RC_OK } proc unmark_cmd {data buffer args} { if {[lindex $args 0] eq "all"} { set ::MARK_LIST [list] weechat::print $buffer "all channels was removed from notify list" return $::weechat::WEECHAT_RC_OK } set channel [weechat::buffer_get_string $buffer localvar_channel] if {[weechat::info_get irc_is_channel $channel] eq "1"} { if {[set index [lsearch $::MARK_LIST $channel]] != -1} { set ::MARK_LIST [lreplace $::MARK_LIST $index $index] weechat::print $buffer "channel \"$channel\" was removed from notify list" } else { weechat::print $buffer "channel \"$channel\" not on notify list" } } else { weechat::print $buffer "this command must be executed on channel" } return $::weechat::WEECHAT_RC_OK } proc smark_cmd {data buffer args} { set channel [weechat::buffer_get_string $buffer localvar_channel] if {[weechat::info_get irc_is_channel $channel] eq "1"} { if {[set index [lsearch $::MARK_LIST $channel]] == -1} { lappend ::MARK_LIST $channel weechat::print $buffer "channel \"$channel\" was appended to notify list" } else { set ::MARK_LIST [lreplace $::MARK_LIST $index $index] weechat::print $buffer "channel \"$channel\" was removed from notify list" } } else { weechat::print $buffer "this command must be executed on channel" } return $::weechat::WEECHAT_RC_OK } proc signal_proc {data signal irc_msg} { if {[regexp {.+@.+\sPRIVMSG\s(#.+)\s:.+} $irc_msg wh channel] == 1} { if {[lsearch $::MARK_LIST $channel] != -1} { set buffer [weechat::current_buffer] if {$channel ne [weechat::buffer_get_string $buffer localvar_channel]} { weechat::print $buffer "$::SCRIPT_NAME: there is new message on $channel" weechat::hook_signal_send weechat_highlight $::weechat::WEECHAT_HOOK_SIGNAL_STRING $channel } else { if {$::HL_ON_CUR_CHAN} { weechat::hook_signal_send weechat_highlight $::weechat::WEECHAT_HOOK_SIGNAL_STRING $channel } } } } return $::weechat::WEECHAT_RC_OK } weechat::hook_command mark {mark current channel to highlight on each message} {} {} {} mark_cmd {} weechat::hook_command unmark {unmark channel(s)} {[all]} {} {} unmark_cmd {} weechat::hook_command smark {unmark channel(s)} {[all]} {} {} smark_cmd {} weechat::hook_signal *,irc_in_PRIVMSG signal_proc {} weechat::hook_config plugins.var.tcl.chan_hl.hl_on_cur_chan config_changed {} weechat-scripts/tcl/xosdnotify.tcl0000644000175100017510000001207115112622420016337 0ustar manumanu# Copyright (c) 2010-2013 by Dmitry Kobylin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # show private/highlight messages with OSD # # 2013-03-28, Dmitry Kobylin : # version 0.2 # * add /xosdtest command # * add config change hook # 2010-09-29, Dmitry Kobylin : # version 0.1 # # set SCRIPT xosdnotify set VERSION 0.2 weechat::register $SCRIPT {Dmitry Kobylin } $VERSION GPL3 {show OSD on highlight/private message} {} {} package require tclxosd # default values set default_blink on ;# blink of OSD set default_blink_interval 700 ;# interval of blinking set default_blink_count 4 ;# count of blinks before OSD hides, don't set to 0 set default_lines 1 ;# number of lines in OSD that can be shown simultaneously set default_align {left bottom} ;# align of OSD set default_offset {16 16} ;# padding of OSD from screen edge set default_font -*-fixed-*-*-*-*-*-200-*-*-*-*-*-* ;# font of OSD set default_encoding utf-8 set default_color #ffff00 # check config and set default values if necessary if {[weechat::config_get_plugin blink] eq ""} {weechat::config_set_plugin blink $default_blink} if {[weechat::config_get_plugin blink_interval] eq ""} {weechat::config_set_plugin blink_interval $default_blink_interval} if {[weechat::config_get_plugin blink_count] eq ""} {weechat::config_set_plugin blink_count $default_blink_count} if {[weechat::config_get_plugin lines] eq ""} {weechat::config_set_plugin lines $default_lines} if {[weechat::config_get_plugin align] eq ""} {weechat::config_set_plugin align $default_align} if {[weechat::config_get_plugin offset] eq ""} {weechat::config_set_plugin offset $default_offset} if {[weechat::config_get_plugin font] eq ""} {weechat::config_set_plugin font $default_font} if {[weechat::config_get_plugin encoding] eq ""} {weechat::config_set_plugin encoding $default_encoding} if {[weechat::config_get_plugin color] eq ""} {weechat::config_set_plugin color $default_color} # create xosd command set osd [xosd::create [weechat::config_get_plugin lines]] $osd align {*}[split [weechat::config_get_plugin align]] $osd offset {*}[split [weechat::config_get_plugin offset]] $osd font [weechat::config_get_plugin font] $osd color [weechat::config_get_plugin color] proc private_msg {osd signal msg} { if {[regexp {:(.+)!.+@.+\s+PRIVMSG\s.+:(.+)} \ [weechat::iconv_from_internal [weechat::config_get_plugin encoding] $msg] -> nick msg]} { $osd text 0 "$nick: $msg" } set n [expr {[weechat::config_get_plugin blink_count] * 2}] weechat::hook_timer [weechat::config_get_plugin blink_interval] 0 $n timer [list $osd [incr ::key]] return $::weechat::WEECHAT_RC_OK } proc highlight_msg {osd signal msg} { $osd text 0 [weechat::iconv_from_internal [weechat::config_get_plugin encoding] $msg] set n [expr {[weechat::config_get_plugin blink_count] * 2}] weechat::hook_timer [weechat::config_get_plugin blink_interval] 0 $n timer [list $osd [incr ::key]] return $::weechat::WEECHAT_RC_OK } proc osd_test {osd buffer args} { if {[string length [set s [join $args]]] == 0} { set s test } $osd text 0 [weechat::iconv_from_internal [weechat::config_get_plugin encoding] $s] set n [expr {[weechat::config_get_plugin blink_count] * 2}] weechat::hook_timer [weechat::config_get_plugin blink_interval] 0 $n timer [list $osd [incr ::key]] } proc timer {data count} { lassign $data osd key # don't response to old timer if {$key < $::key} { return $::weechat::WEECHAT_RC_OK } if {[weechat::config_get_plugin blink] != "off"} { if {[$osd onscreen]} { $osd hide } else { $osd show } } if {$count == 0} { $osd hide } return $::weechat::WEECHAT_RC_OK } proc config_changed {osd option value} { $osd align {*}[split [weechat::config_get_plugin align]] $osd offset {*}[split [weechat::config_get_plugin offset]] $osd font [weechat::config_get_plugin font] $osd color [weechat::config_get_plugin color] return $::weechat::WEECHAT_CONFIG_OPTION_SET_OK_CHANGED } set ::key 0 # hook signals weechat::hook_signal irc_pv private_msg $osd weechat::hook_signal weechat_highlight highlight_msg $osd # hook test command weechat::hook_command xosdtest {show test message with OSD} {msg} {} {} osd_test $osd # hook config change weechat::hook_config plugins.var.tcl.$SCRIPT\.* config_changed $osd weechat-scripts/tcl/ipinfo.tcl0000644000175100017510000001006715112622420015420 0ustar manumanu# Copyright (c) 2022 by CrazyCat # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # --------------------------------------------- # Retrieve informations about an IP # # Usage : /ipinfo 132.54.12.32 # --------------------------------------------- # History # 2022-02-24 : Initial release set SCRIPT_VERSION 1.0 set SCRIPT_NAME ipinfo weechat::register $SCRIPT_NAME {CrazyCat } $SCRIPT_VERSION GPL3 {retrieve informations about an IP} {} {} weechat::hook_command ipinfo {retrieve informations about an IP} {} {Do /set *ipinfo* for settings} {} ipinfo {} if {[set output [weechat::config_get_plugin output]] eq ""} { weechat::config_set_plugin output "CITY (COUNTRY) - ISP" } if {[set output [weechat::config_get_plugin lang]] eq ""} { weechat::config_set_plugin lang "en" } # Small variables for utility set mask(ipv4) {^(?:25[0-5]|2[0-4]\d|[0-1]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d{1,2})){3}$} set mask(ipv6) {^([[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}|::|:(?::[[:xdigit:]]{1,4}){1,6}|[[:xdigit:]]{1,4}:(?::[[:xdigit:]]{1,4}){1,5}|(?:[[:xdigit:]]{1,4}:){2}(?::[[:xdigit:]]{1,4}){1,4}|(?:[[:xdigit:]]{1,4}:){3}(?::[[:xdigit:]]{1,4}){1,3}|(?:[[:xdigit:]]{1,4}:){4}(?::[[:xdigit:]]{1,4}){1,2}|(?:[[:xdigit:]]{1,4}:){5}:[[:xdigit:]]{1,4}|(?:[[:xdigit:]]{1,4}:){1,6}:)$} # this will be used later variable private {"0.0.0.0/8" "10.0.0.0/8" "100.64.0.0/10" "127.0.0.0/8" "169.254.0.0/16" "172.16.0.0/12" \ "192.0.0.0/24" "192.0.2.0/24" "192.88.99.0/24" "162.168.0.0/16" "192.168.18.0.0/15" "198.51.100.0/24" \ "203.0.113.0/24" "224.0.0.4/24" "233.252.0.0/24" "240.0.0.0/4" "255.255.255.255/32"} proc ipinfo {data buffer args} { set ip [string trim [join $args]] set ::ipinfo(buffer) $buffer if {$ip eq ""} { weechat::print $buffer "Error : Syntax is /ipinfo ip.to.check" return $::weechat::WEECHAT_RC_ERROR } if {![isip $ip]} { weechat::print $buffer [format "*** %s$ip%s is not a valid IP address" [weechat::color "red"] [weechat::color "default"]] return $::weechat::WEECHAT_RC_ERROR } weechat::hook_process "url:http://ip-api.com/json/$ip?fields=status,message,continent,country,city,zip,lat,lon,timezone,isp,org,reverse,mobile,proxy,hosting,query&lang=[weechat::config_get_plugin lang]" 5000 "ipinfo_process_cb" "" return $::weechat::WEECHAT_RC_OK } proc ipinfo_process_cb {data command rc out err} { set infos [json2dict $out] if {![dict exists $infos status]} { weechat::print $::ipinfo(buffer) [format "*** %sERROR%s : Cannot retrieve informations from ip-api.com" [weechat::color "red"] [weechat::color "default"]] return $::weechat::WEECHAT_RC_ERROR } if {[dict get $infos status]=="fail"} { weechat::print $::ipinfo(buffer) [format "*** %sERROR%s : [dict get $infos message]" [weechat::color "red"] [weechat::color "default"]] return $::weechat::WEECHAT_RC_ERROR } set ip [dict get $infos query] set myout [string map [list "CITY" [dict get $infos city]\ "COUNTRY" [dict get $infos country]\ "ISP" [dict get $infos isp]\ ] [weechat::config_get_plugin output]] weechat::print $::ipinfo(buffer) [format "*** IP infos for %s$ip%s: $myout" [weechat::color "red"] [weechat::color "default"]] return $::weechat::WEECHAT_RC_OK } proc isip { ip } { if {[regexp $::mask(ipv4) $ip ipv4]} { return true } elseif {[regexp $::mask(ipv6) $ip ipv6]} { return true } else { return false } } proc json2dict {JSONtext} { string range [string trim [string trimleft [string map {\t {} \n {} \r {} , { } : { } \[ \{ \] \}} $JSONtext] {\uFEFF}]] 1 end-1 } weechat-scripts/javascript/0000755000175100017510000000000015112622421015011 5ustar manumanuweechat-scripts/javascript/autospurdo.js0000644000175100017510000000714415112622421017562 0ustar manumanu/* +Copyright (c) 2015 by installgen2 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty ofh +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ weechat.register("autospurdo", "installgen2", "1.0", "GPL3", "Speak in spurdo", "", ""); weechat.hook_command("spurdosay", "Say message in spurdo", "", "", "", "spurdoSay", ""); weechat.hook_command("togglespurdo", "Toggle automatic spurdo conversion", "", "", "", "toggleSpurdo", ""); weechat.hook_command_run("/input return", "spurdoModifier", ""); function spurdoSay (data, buffer, args) { var message = toSpurdo(args); weechat.command("", "/say " + message) return weechat.WEECHAT_RC_OK; } var spurdoEnabled = false; function spurdoModifier (data, buffer, command) { if (!spurdoEnabled || command != "/input return") return weechat.WEECHAT_RC_OK; text = weechat.buffer_get_string(buffer, "input"); if (text[0] == "/") return weechat.WEECHAT_RC_OK; weechat.buffer_set(buffer, "input", toSpurdo(text)) return weechat.WEECHAT_RC_OK; } function toggleSpurdo (data, buffer, args) { if (spurdoEnabled) { spurdoEnabled = false; weechat.print("", "Disabled automatic spurdo"); } else { spurdoEnabled = true; weechat.print("", "Enabled automatic spurdo"); } return weechat.WEECHAT_RC_OK; } // The following shit is from https://github.com/installgen2/libspurdo // Return a random ebin face var ebinFaces = [":D", ":DD", ":DDD", ":-D", ":-DD", "XD", "XXD", "XDD", "XXDD"]; function getEbinFace () { return ebinFaces[Math.floor(Math.random() * ebinFaces.length)]; } // define replacements var replacements = [ ["wh", "w"], ["th", "d"], ["af", "ab"], ["ap", "ab"], ["ca", "ga"], ["ck", "gg"], ["co", "go"], ["ev", "eb"], ["ex", "egz"], ["et", "ed"], ["iv", "ib"], ["it", "id"], ["ke", "ge"], ["nt", "nd"], ["op", "ob"], ["ot", "od"], ["po", "bo"], ["pe", "be"], ["pi", "bi"], ["up", "ub"], ["va", "ba"], ["ck", "gg"], ["cr", "gr"], ["kn", "gn"], ["lt", "ld"], ["mm", "m"], ["nt", "dn"], ["pr", "br"], ["ts", "dz"], ["tr", "dr"], ["bs", "bz"], ["ds", "dz"], ["es", "es"], ["fs", "fz"], ["gs", "gz"], [" is", " iz"], ["ls", "lz"], ["ms", "mz"], ["ns", "nz"], ["rs", "rz"], ["ss", "sz"], ["ts", "tz"], ["us", "uz"], ["ws", "wz"], ["ys", "yz"], ["alk", "olk"], ["ing", "ign"], ["ic", "ig"], ["ng", "nk"], ["kek", "geg"], ["epic", "ebin"], ["some", "sum"], ["meme", "maymay"], ]; function toSpurdo (string) { // Convert to lowercase (TODO: add upercase handling) string = string.toLowerCase(); // apply replacements replacements.forEach(function(filter) { var replaceFrom = new RegExp(filter[0], "gm"), replaceTo = filter[1]; string = string.replace(replaceFrom, replaceTo); }); // Replace "," and "." with ebin faces while (string.match(/\.|,(?=\s|$)/m)) { string = string.replace(/\.|,(?=\s|$)/m, " " + getEbinFace()); } // append an ebin face if not found var ebinFaceFound = false; ebinFaces.forEach(function (face) { if (string.indexOf(face) != -1) { ebinFaceFound = true; } }); if (!ebinFaceFound) { string += " " + getEbinFace(); } // return spurdo'd text return string; } weechat-scripts/javascript/opall.js0000644000175100017510000000370215112622421016460 0ustar manumanuname = "opall"; author = "gagz@riseup.net"; version = "0.2"; license = "wtfpl"; description = "op people using chanserv instead of /mode"; shutdown_function = ""; charset = ""; weechat.register(name, author, version, license, description, shutdown_function, charset); weechat.hook_command("opall", "OP everybody on the channel, using chanserv instead of /mode", "", "", "", "chanserv_op_all", ""); function chanserv_op_all() { var buffer = weechat.current_buffer() var chan = weechat.buffer_get_string(buffer, "localvar_channel") // we must be sure to be on an IRC buffer if( weechat.buffer_get_string(buffer, "plugin") != "irc" ) { weechat.print("", "Works only on IRC channels") return weechat.WEECHAT_RC_ERROR } // lets get the nicklist of the current buffer var nicklist = weechat.infolist_get("nicklist", buffer, ""); // and walk through it while( weechat.infolist_next(nicklist) ) { var type = weechat.infolist_string(nicklist, "type"); var visible = weechat.infolist_integer(nicklist, "visible"); var prefix = weechat.infolist_string(nicklist, "prefix"); // we are only interested in actual non-op visible nicks // TODO: find a more reliable way to op non-op users (ie. prefix // can be changed in the settings and might not be "@") // TODO: check the IRC server/services version to talk with // chanserv correctly. This works with charybdis/atheme. if( type == "nick" && visible == 1 && prefix != "@") { var nick = weechat.infolist_string(nicklist, "name"); var command = "/msg chanserv op " + chan + " " + nick; weechat.print("", command); weechat.command(buffer, command); } } weechat.infolist_free(nicklist); return weechat.WEECHAT_RC_OK; } weechat-scripts/guile/0000755000175100017510000000000015112622421013750 5ustar manumanuweechat-scripts/guile/karmastorm.scm0000644000175100017510000000307115112622421016635 0ustar manumanu; Copyright (c) 2014 by msoucy ; ; This program is free software; you can redistribute it and/or modify ; it under the terms of the GNU General Public License as published by ; the Free Software Foundation; either version 3 of the License, or ; (at your option) any later version. ; ; This program is distributed in the hope that it will be useful, ; but WITHOUT ANY WARRANTY; without even the implied warranty of ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ; GNU General Public License for more details. ; ; You should have received a copy of the GNU General Public License ; along with this program. If not, see . ; ; ; (this script requires WeeChat 0.4.1 or newer) ; (weechat:register "karmastorm" "Matt Soucy" "0.3" "GPL3" "KarmaStorm" "" "") ; Hook main function up to the /emote command (weechat:hook_command "karmastorm" "Karma Storm" "/karmastorm num names..." "Increment karma for the given names" "" "main" "") ; Increments karma for all users provided (define (main data buffer command) (letrec ((toked (string-tokenize command)) (incr (lambda (n) (weechat:command buffer (string-append n "++")))) (lp (lambda (num) (if (not (zero? num)) (begin (map incr (cdr toked)) (lp (- num 1)) )))) (err (lambda () ((weechat:print "" "Expected arguments for karmastorm"))))) (if (null? toked) err (lp (string->number (car toked))))) weechat:WEECHAT_RC_OK) weechat-scripts/guile/emote.scm0000644000175100017510000000563515112622421015576 0ustar manumanu; Copyright (c) 2014 by csmith ; ; This program is free software; you can redistribute it and/or modify ; it under the terms of the GNU General Public License as published by ; the Free Software Foundation; either version 3 of the License, or ; (at your option) any later version. ; ; This program is distributed in the hope that it will be useful, ; but WITHOUT ANY WARRANTY; without even the implied warranty of ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ; GNU General Public License for more details. ; ; You should have received a copy of the GNU General Public License ; along with this program. If not, see . ; ; ; (this script requires WeeChat 0.4.1 or newer) ; ; History: ; 2023-03-18, Yuval Langer ; version 0.3.1: Replace `apply string-append` with `string-join`. ; 2017-02-18, nycatelos ; version 0.3: added more emotes ; 2016-06-03, nycatelos ; version 0.2: added additional emotes ; 2014-05-03, csmith ; version 0.1: initial release (use-modules (srfi srfi-69)) (weechat:register "emote" "Caleb Smith" "0.3.1" "GPL" "Emote" "" "") ; Mappings of words with their emoticons (define patterns (alist->hash-table '( ("tableflip" . "(╯° °)╯︵ ┻━┻)") ("rageflip" . "(ノಠ益ಠ)ノ彡┻━┻") ("doubleflip" . "┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻") ("disapproval" . "ಠ_ಠ") ("sun" . "☼") ("kitaa" . "キタ━━━(゜∀゜)━━━!!!!!") ("joy" . "◕‿◕") ("nyancat" . "~=[,,_,,]:3") ("lenny" . "( ͡° ͜ʖ ͡°)") ("shrug" . "¯\\_(ツ)_/¯") ("denko" . "(・ω・)") ("tableplace" . "┬─┬ ノ( ゜-゜ノ)") ("gface" . "( ≖‿≖)") ("facepalm" . "(-‸ლ)") ("tehe" . "☆~(◡﹏◕✿)") ("angry" . "(╬ ಠ益ಠ)") ("umu" . "( ̄ー ̄)") ("toast" . "( ^_^)o自自o(^_^ )") ("yay" . "ヽ(´ー`)ノ") ))) ; Derive the tab completion string for the subcommands. (define tab-completions (string-join (map (lambda (i) (string-append "|| " i)) (hash-table-keys patterns)))) ; Hook main function up to the /emote command (weechat:hook_command "emote" "Emote" "/emote phrase" (string-append "" "\nUse `/emote phrase`. Words in phrase will be replaced with their" "\nemoticons:" "\n" "\nExamples:" "\n /emote tableflip - (╯° °)╯︵ ┻━┻)" "\n /emote look - ಠ_ಠ") tab-completions "main" "") ; Handle the IRC command given by the user. Sets input buffer as a side-effect (define (main data buffer command) (weechat:buffer_set buffer "input" (string-join (map (lambda (c) (string-append (hash-table-ref/default patterns c c) " ")) (string-tokenize command)))) weechat:WEECHAT_RC_OK) weechat-scripts/guile/weechataboo.scm0000644000175100017510000001430415112622421016737 0ustar manumanu; WeeChat-Script to replace emotion-tags with random emoticons. ; Copyright (C) 2017-2018 Alvar ; ; This program is free software: you can redistribute it and/or modify ; it under the terms of the GNU General Public License as published by ; the Free Software Foundation, either version 3 of the License, or ; (at your option) any later version. ; ; This program is distributed in the hope that it will be useful, ; but WITHOUT ANY WARRANTY; without even the implied warranty of ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ; GNU General Public License for more details. ; ; You should have received a copy of the GNU General Public License ; along with this program. If not, see . ; ; For usage see `/help weechataboo` (use-modules (srfi srfi-1)) ; -> List ; Returns a list of available emotions. (define (emotion-categories) (string-split (weechat:config_get_plugin "emotions") #\,)) ; String -> String ; Returns an emoticon for a known emotion. (define (emotion->emoticon emo) (let ((emotions (string-split (weechat:config_get_plugin emo) #\,)) (random-emotion (lambda (l) (list-ref l (random (length l)))))) (random-emotion emotions))) ; String -> String ; Replaces in the given string every ~~EMOTION with a fitting emoticon. (define (emoticonize-line line) (let* ((as-tag (lambda (emo) (string-append "~~" emo))) (has-emotions? (lambda (txt) (any (lambda (emo) (number? (string-contains txt (as-tag emo)))) (emotion-categories)))) (replace (lambda (emo txt) (let ((pos (string-contains txt (as-tag emo)))) (if (number? pos) (string-replace txt (emotion->emoticon emo) pos (+ (string-length (as-tag emo)) pos)) txt)))) (new-line (fold replace line (emotion-categories)))) (if (has-emotions? new-line) (emoticonize-line new-line) new-line))) ; Pointer ? ? String -> String ; This function was registered to be called when an input was submitted and ; will try to replace ~~EMOTIONs to emoticons. (define (weechataboo-hook data modifier modifier-data msg) (emoticonize-line msg)) ; Pointer String List -> Weechat-Return ; Function which tells you to RTFM. (define (weechataboo-func data buffer args) (weechat:print "" "See /help weechataboo") weechat:WEECHAT_RC_OK) ; -> () ; Function to be executed when there is no config yet. Creates a dummy one. (define (initial-setup) (let* ; Some defaults which may be useful‥ ((emotions '(("aggressive" "o(-`д´- 。),凸ಠ益ಠ)凸,' ̿'̵͇̿̿з=(◕_◕)=ε/̵͇̿̿/'̿'̿ ̿,O=('-'Q),。゜(`Д´)゜。,┌∩┐(ಠ_ಠ)┌∩┐") ("angry" "눈_눈,(¬_¬),(`ε´),(¬▂¬),(▽д▽),ಠ_ರೃ,(⋋▂⋌),(‡▼益▼),(*`へ´*)") ("blush" "(´ω`*),(‘-’*),(/ε\*),(*゚∀゚*),(*´ェ`*)") ("cat" "≋≋≋≋≋̯̫⌧̯̫(ˆ•̮ ̮•ˆ)") ("cry" "(;へ:),(πーπ),(iДi),(;Д;),(╥_╥),ಥ╭╮ಥ") ("dance" "ヾ(^^ゞ),(ノ^o^)ノ,⌎⌈╹우╹⌉⌍,└|゚ε゚|┐,┌|゚з゚|┘,(〜 ̄△ ̄)〜") ("drink" "(^-^)_日,(*^◇^)_旦,(  ゜Д゜)⊃旦,~~旦_(-ω-`。)") ("excited" "(≧∇≦*),ヽ(^Д^)ノ,(* >ω<)") ("gross" "(咒),( ≖ิ‿≖ิ ),ʅ(◔౪◔ ) ʃ") ("happy" "≖‿≖,(^ω^),(^ω^),ヽ(ヅ)ノ,(¬‿¬),(◡‿◡✿),(❀◦‿◦),(⁎⚈᷀᷁ᴗ⚈᷀᷁⁎)") ("heart" "♡^▽^♡,✿♥‿♥✿,(。♥‿♥。),ヽ(o♡o)/,(◍•ᴗ•◍)❤,(˘︶˘).。.:*♡,❣◕ ‿ ◕❣") ("hug" "⊂(・﹏・⊂),(っ´▽`)っ,(づ ̄ ³ ̄)づ,⊂(´・ω・`⊂)") ("kiss" "|°з°|,(*^3^),(´ε`*),(っ˘з(˘⌣˘ ),(*^3^)/~♡") ("lenny" "( ͡ ͜ʖ ͡ ),( ͡~ ͜ʖ ͡°),( ͡~ ͜ʖ ͡~),ヽ( ͝° ͜ʖ͡°)ノ,(つ ͡° ͜ʖ ͡°)つ,( ͝סּ ͜ʖ͡סּ),") ("magic" "(っ・ω・)っ≡≡≡≡≡≡☆,ヽ༼ຈل͜ຈ༽⊃─☆*:・゚") ("sheep" "@^ェ^@,@・ェ・@") ("shock" "(゚д゚;)") ("shrug" "┐(´д`)┌,╮(╯∀╰)╭,┐(´∀`)┌,ʅ(́◡◝)ʃ,ヽ(~~~ )ノ,ヽ(。_°)ノ,¯\(◉◡◔)/¯,◔_◔") ("shy" "(/ω\),(‘-’*),(´~`ヾ),(〃´∀`)") ("smug" "( ̄ω ̄),( ̄ー ̄),( ̄ー ̄),(^~^)") ("sword" "╰(◕ヮ◕)つ¤=[]———,╰(⇀︿⇀)つ-]═───,∩(˵☯‿☯˵)つ¤=[]:::::>") ("wink" "ヾ(^∇^),ヾ(☆▽☆),(。-ω-)ノ,( ・ω・)ノ"))) (names (string-join (map car emotions) ","))) (and (weechat:config_set_plugin "emotions" names) (for-each (lambda (emo) (weechat:config_set_plugin (car emo) (cadr emo))) emotions)))) ; -> Weechat-Return ; Function to be called when the plugin is unloaded. Will hopefully clean ; up all settings. (define (clean-up) (for-each weechat:config_unset_plugin (emotion-categories)) (weechat:config_unset_plugin "emotions") weechat:WEECHAT_RC_OK) (weechat:register "weechataboo" "Alvar" "0.1.2" "GPL3" "Replace emotion-tags with random emoticons" "clean-up" "") (and (eq? (weechat:config_is_set_plugin "emotions") 0) (initial-setup)) (weechat:hook_modifier "irc_out1_privmsg" "weechataboo-hook" "") (weechat:hook_command "weechataboo" (string-append "This script automatically replaces written emotion-keywords\n" "with a random emoticon from a list of matching ones. The\n" "keyword must have two tildes (~~) as a prefix.\n" "Example: ~~wink\n\n" "All values are comma separated. Please make sure that every\n" "emotion in the `emotions`-list has its own entry!\n\n" "→ Keywords: /set plugins.var.guile.weechataboo.emotions\n" "→ Emoticons: /set plugins.var.guile.weechataboo.$EMOTION\n") "" "" "" "weechataboo-func" "") weechat-scripts/guile/gateway_rename.scm0000644000175100017510000002634715112622421017460 0ustar manumanu;; -*- geiser-scheme-implementation: 'guile -*- ;; Copyright 2017 by Zephyr Pellerin ;; ------------------------------------------------------------ ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;; History: ;; 1.2 - Use weechat plugin configuration data to match IRC gateways ;; 0.9 - Lookup correct servername in /VERSION ;; 0.8 - Barebones, contained list of translations (use-modules ((srfi srfi-1) #:select (any fold))) (use-modules ((srfi srfi-26) #:select (cut))) (use-modules (ice-9 regex)) (use-modules (ice-9 hash-table)) (use-modules (ice-9 match)) (define *weechat/script-name* "gateway_rename") (define *weechat/script-author* "zv ") (define *weechat/script-version* "1.2.2") (define *weechat/script-license* "GPL3") (define *weechat/script-description* "Convert usernames of gateway connections their real names") ;; A test-harness for checking if we are inside weechat (define-syntax if-weechat (syntax-rules () ((_ conseq alt) (if (defined? 'weechat:register) conseq alt)) ((_ conseq) (if (defined? 'weechat:register) conseq)))) (if-weechat (weechat:register *weechat/script-name* *weechat/script-author* *weechat/script-version* *weechat/script-license* *weechat/script-description* "" "")) ;; `user-prefix' is a distinguishing username prefix for 'fake' users (define *user-prefix* "^") (define *gateway-config* "gateways") (define *default-irc-gateways* "(freenode #radare r2tg ) (freenode #test-channel zv-test NICK:)") (define (print . msgs) (if (defined? 'weechat:print) (weechat:print "" (apply format (cons #f msgs))))) ;; A regular expression must have the gateway username in the first matchgroup, ;; the "real" username in the 3rd, and the real-username along with it's enclosing ;; brackets in the 2nd (define *gateway-regexps* (make-hash-table)) (define (process-network-infolist) "Convert the internal user-defined servername to the 'true' servername returned during /version" (define il (weechat:infolist_get "irc_server" "" "")) ;; pull the network field out of the list of /VERSION results (define (extract-network result) (if (null? result) #f (match (string-split (car result) #\=) [("NETWORK" network) network] [_ (extract-network (cdr result))]))) ;; pull out a '(name network-name) pair from an infolist str (define (process return-code) (if (= return-code 0) '() (let* ((name (weechat:infolist_string il "name")) (isupport (weechat:infolist_string il "isupport")) (reply (string-split isupport #\space)) (network (or (extract-network reply) ;; if no network, use local name name))) (cons (cons name network) (process (weechat:infolist_next il)))))) (let ((result (process (weechat:infolist_next il)))) (weechat:infolist_free il) result)) ;; This is a table that maps a weechat network 'name' to it's IRC-style hostname (define *hostname-table* (alist->hash-table '(("freenode" . "freenode")))) (if-weechat (set! *hostname-table* (alist->hash-table (process-network-infolist)))) (define (replace-privmsg msg gateways) "A function to replace the PRIVMSG sent by by a gateway " (let* ((match? (cut regexp-exec <> msg)) (result (any match? gateways))) (if result (let* ([nth-match (cut match:substring result <>)] ;; take everything after username before message [username (nth-match 1)] [real-username (nth-match 3)] ;; Extract everything after the gateway-user mask [raw-message (string-copy msg (match:end result 2) (string-length msg))] ;; .. and be sure to strip any preceding characters [message (string-trim raw-message)] ;; extract everything before the message but after the username [hostmask (string-copy msg (match:end result 1) (match:start result 2))]) (string-append ":" *user-prefix* real-username hostmask message)) msg))) (define (server->gateways server) (hash-ref *gateway-regexps* (hash-ref *hostname-table* server))) (define (privmsg-modifier data modifier-type server msg) "The hook for all PRIVMSGs in Weechat" (let ((gateways (server->gateways server))) (if gateways (replace-privmsg msg gateways) msg))) (define* (make-gateway-regexp gateway-nick channel mask #:optional emit-string) "Build a regular expression that will match the nick, channel and \"\"-style mask" (let* ([mask-regexp ;; replace with <(\\S*?)> (regexp-substitute/global #f "NICK" mask 'pre "(\\S*?)" 'post "")] [composed-str (format #f ":(~a)!\\S* PRIVMSG ~a :(~a)" gateway-nick (if (equal? "*" channel) "\\S*" channel) mask-regexp)]) (if emit-string composed-str (make-regexp composed-str)))) (define (extract-gateway-fields str) "This is a hack around Guile's non-greedy matchers. # Example scheme@(guile-user)> (extract-gateway-fields \"(freenode #radare r2tg )\") $1 = (\"freenode\" \"#radare\" \"r2tg\" \"\")" (let* ((range-end (λ (range) (+ 1 (cdr range)))) (find-space (λ (end) (string-index str #\space end))) ;; opening (first) and closing (last) parenthesis (opening-par (string-index str #\()) (closing-par (string-index str #\))) ;; extract the range of each (server (cons (+ 1 opening-par) (find-space 0))) (channel (cons (range-end server) (find-space (range-end server)))) (gateway-nick (cons (range-end channel) (find-space (range-end channel)))) (mask (cons (range-end gateway-nick) closing-par))) ;; and then get the strings (map (λ (window) (substring str (car window) (cdr window))) (list server channel gateway-nick mask)))) (define* (process-weechat-option opt #:optional emit-string) "Takes in the application-define weechat-options and emits a server and matching regular expression. The optional parameter `emit-string' controls if a string or a compiled regular expression is returned. # Example scheme@(guile-user)> (process-weechat-option \"(freenode #radare r2tg )\") $1 = '(\"freenode\" . (make-regexp \":(r2tg)!\\S* PRIVMSG #radare :(<(\\S*?)>) .*\")))" (let* ((fields (extract-gateway-fields opt)) (server (list-ref fields 0)) (channel (list-ref fields 1)) (gateway-nick (list-ref fields 2)) (mask (list-ref fields 3))) (cons server (make-gateway-regexp gateway-nick channel mask emit-string)))) (define (split-gateways config) "Push our elts onto the stack to extract our configs # Example scheme@(guile-user)> (split-gateways \"(freenode #radare r2tg )(* * slack-irc-bot NICK:)\") $1 = (\"(freenode #radare r2tg )\" \"(* * slack-irc-bot NICK:)\") " (define (process stk current rest) (if (string-null? rest) (cons current '()) (let* ((head (string-ref rest 0)) (nrest (string-drop rest 1)) (ncurrent (string-append current (string head)))) (cond [(and (null? stk) (not (string-null? current))) (cons current (process stk "" rest))] [(eq? head #\() (process (cons #\( stk) ncurrent nrest)] [(eq? head #\)) (process (cdr stk) ncurrent nrest)] ;; skip characters if our stk is empty [(null? stk) (process stk current nrest)] [else (process stk ncurrent nrest)])))) (process '() "" config)) (define (fetch-weechat-gateway-config) "Extract the gateway configuration string" (if-weechat (weechat:config_get_plugin *gateway-config*) *default-irc-gateways*)) (define (assign-gateways-regex) "Fetch our weechat gateway configuration and assign it to our local regexps" (let* ((config_str (fetch-weechat-gateway-config)) (config_lst (split-gateways config_str)) (gateways (map process-weechat-option config_lst))) ;; for each gateway, add it to our `*gateway-regexps*' ht (for-each (λ (gt) (let* ((server (car gt)) (new-regex (cdr gt)) (server-regexps (hash-ref *gateway-regexps* server '()))) (hash-set! *gateway-regexps* server (cons new-regex server-regexps)))) gateways))) ;; Initialize our weechat settings & privmsg hook (define (renamer_command_cb data buffer args) weechat::WEECHAT_RC_OK) (if-weechat (begin (if (not (= 1 (weechat:config_is_set_plugin *gateway-config*))) (weechat:config_set_plugin *gateway-config* *default-irc-gateways*)) (weechat:hook_modifier "irc_in_privmsg" "privmsg-modifier" "") (weechat:hook_command *weechat/script-name* *weechat/script-description* "" ;; arguments " There are many IRC gateway programs that, rather than sending as if they were another user, simply prepend the name of the user that is using that gateway to the messages they are sending. For example: `slack-irc-bot` might send a message to #weechat: slack-irc-bot: How about them Yankees? gateway_rename intercepts that message and converts it to: ^zv: How about them Yankees? (gateway_rename prefixes the `^' (caret) symbol to each message to prevent message spoofing) Adding a Renamer: Which servers, channels, users and nickname templates are renamed can all be modified in `plugins.var.guile.gateway_rename.gateways' Two gateways are matched by default, but are primarily intended to serve as a template for you to add others. Each gateway renamer is placed inside of a set of parenthesis and contain four fields respectively: 1. IRC server name (use the same name that weechat uses) 2. Channel 3. Gateway's nick/user name 4. The last field is a template for how to match the nickname of the 'real user' For example, if you wanted to convert the message 'gateway-bot: zv: Yes' into 'zv: Yes' You would set the last field to 'NICK:' because each NICK at the beginning of the message is suffixed with a `:' " "" "renamer_command_cb" ""))) ;; Setup our gateways->regex map (assign-gateways-regex) ;;(print "Gateway Nickconverter by zv ") weechat-scripts/python/0000755000175100017510000000000015112622410014162 5ustar manumanuweechat-scripts/python/weemustfeed.py0000644000175100017510000004065215112622403017062 0ustar manumanu# -*- coding: utf-8 -*- # Licensed under the MIT license: # Copyright (c) 2013 Bit Shift # Copyright (c) 2016 Pol Van Aubel # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # http://www.opensource.org/licenses/mit-license.php # # Revision log: # 0.3 Pol Van Aubel # Make Python3 compatible. # Feed fetching should now timeout after 15 seconds. # Handle negative return values correctly. # Fix deleting from active iteration object in unload. # # 0.2.3 Pol Van Aubel # Changed weechat.prnt to weechat.prnt_date_tags where messages # are not a direct result from user input; errors get tagged with # irc_error and notify_message, messages get tagged with # notify_message. # # 0.2.2 Version from Bit Shift import weechat import string import feedparser import sys # Only required for python version check. PY2 = sys.version_info < (3,) weechat.register( "weemustfeed", "Bit Shift ", "0.3", "MIT", "RSS/Atom/RDF aggregator for weechat", "", "" ) default_settings = { "interval": "300", "feeds": "" } weemustfeed_buffer = None weemustfeed_timer = None fetch_hooks = {} updating = set() partial_feeds = {} help_message = """ COMMANDS: a Add a feed with display name of and URL of . d Delete the feed with display name . u Update the feed with display name to use URL . l List all feeds known to WeeMustFeed. t Toggle a feed - disable/enable it temporarily without fully removing it. ? Display this help message. CONFIG: plugins.var.python.weemustfeed.interval Interval between update checks, in seconds. Must be a number, but is stored as a string. Blame the scripting API. default: "300" """.strip() def show_help(): for line in help_message.split("\n"): weechat.prnt(weemustfeed_buffer, "\t\t" + line) def weemustfeed_input_cb(data, buffer, input_data): global updating chunks = input_data.split() if chunks[0] == "a": if len(chunks) != 3: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Wrong number of parameters. Syntax is 'a '.") return weechat.WEECHAT_RC_ERROR elif any([c not in (string.ascii_letters + string.digits) for c in chunks[1]]): weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Only A-Z, a-z, and 0-9 permitted in names.") return weechat.WEECHAT_RC_ERROR else: current_feeds = weechat.config_get_plugin("feeds").strip().split(";") if chunks[1] in current_feeds: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "A feed with that name already exists (note: feed names are case-insensitive).") return weechat.WEECHAT_RC_ERROR else: current_feeds.append(chunks[1]) weechat.config_set_plugin("feed." + chunks[1].lower() + ".url", chunks[2]) weechat.config_set_plugin("feeds", ";".join(current_feeds)) weechat.prnt(weemustfeed_buffer, "Added '" + chunks[1] + "'.") elif chunks[0] == "d": if len(chunks) != 2: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Wrong number of parameters. Syntax is 'd '.") return weechat.WEECHAT_RC_ERROR elif any([c not in (string.ascii_letters + string.digits) for c in chunks[1]]): weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Only A-Z, a-z, and 0-9 permitted in names.") return weechat.WEECHAT_RC_ERROR else: current_feeds = weechat.config_get_plugin("feeds").strip().split(";") if not chunks[1] in current_feeds: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "No such feed exists.") return weechat.WEECHAT_RC_ERROR else: current_feeds.remove(chunks[1]) weechat.config_set_plugin("feeds", ";".join(current_feeds)) weechat.prnt(weemustfeed_buffer, "Deleted '" + chunks[1] + "'.") elif chunks[0] == "u": if len(chunks) != 3: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Wrong number of parameters. Syntax is 'u '.") return weechat.WEECHAT_RC_ERROR elif any([c not in (string.ascii_letters + string.digits) for c in chunks[1]]): weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Only A-Z, a-z, and 0-9 permitted in names.") return weechat.WEECHAT_RC_ERROR else: current_feeds = weechat.config_get_plugin("feeds").strip().split(";") if not chunks[1] in current_feeds: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "No feed with that name currently exists (note: feed names are case-insensitive).") return weechat.WEECHAT_RC_ERROR else: weechat.config_set_plugin("feed." + chunks[1].lower() + ".url", chunks[2]) weechat.config_set_plugin("feeds", ";".join(current_feeds)) weechat.prnt(weemustfeed_buffer, "Updated '" + chunks[1] + "'.") elif chunks[0] == "l": if len(chunks) != 1: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Wrong number of parameters. Syntax is 'l'.") return weechat.WEECHAT_RC_ERROR else: current_feeds = weechat.config_get_plugin("feeds").strip().split(";") for feed in current_feeds: if feed != "": if (weechat.config_is_set_plugin("feed." + feed.lower() + ".enabled") and weechat.config_get_plugin("feed." + feed.lower() + ".enabled").lower() != "yes"): feed_status = "disabled" elif not (weechat.config_is_set_plugin("feed." + feed.lower() + ".last_id") and weechat.config_get_plugin("feed." + feed.lower() + ".last_id") != ""): feed_status = "new" elif feed in updating: feed_status = "updating" elif (weechat.config_is_set_plugin("feed." + feed.lower() + ".enabled") and weechat.config_get_plugin("feed." + feed.lower() + ".enabled").lower() != "yes"): feed_status = "disabled" else: feed_status = "enabled" weechat.prnt(weemustfeed_buffer, "\t" + feed + ": " + weechat.config_get_plugin("feed." + feed.lower() + ".url") + " [" + feed_status + "]") elif chunks[0] == "t": if len(chunks) != 2: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Wrong number of parameters. Syntax is 't '.") return weechat.WEECHAT_RC_ERROR elif any([c not in (string.ascii_letters + string.digits) for c in chunks[1]]): weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Only A-Z, a-z, and 0-9 permitted in names.") return weechat.WEECHAT_RC_ERROR else: current_feeds = weechat.config_get_plugin("feeds").strip().split(";") if not chunks[1] in current_feeds: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "No such feed exists.") return weechat.WEECHAT_RC_ERROR else: if not weechat.config_is_set_plugin("feed." + chunks[1].lower() + ".enabled"): feed_enabled = True else: feed_enabled = (weechat.config_get_plugin("feed." + chunks[1].lower() + ".enabled").lower() == "yes") if feed_enabled: weechat.config_set_plugin("feed." + chunks[1].lower() + ".enabled", "no") weechat.prnt(weemustfeed_buffer, "Disabled '" + chunks[1] + "'.") else: weechat.config_set_plugin("feed." + chunks[1].lower() + ".enabled", "yes") weechat.prnt(weemustfeed_buffer, "Enabled '" + chunks[1] + "'.") elif chunks[0] == "?": if len(chunks) != 1: weechat.prnt(weemustfeed_buffer, weechat.prefix("error") + "Wrong number of parameters. Syntax is '?'.") return weechat.WEECHAT_RC_ERROR else: show_help() return weechat.WEECHAT_RC_OK def weemustfeed_close_cb(data, buffer): global weemustfeed_buffer, weemustfeed_timer weemustfeed_buffer = None weechat.unhook(weemustfeed_timer) for feed in list(fetch_hooks): weechat.unhook(fetch_hooks[feed]) del fetch_hooks[feed] weemustfeed_timer = None return weechat.WEECHAT_RC_OK def weemustfeed_command_cb(data, buffer, args): global weemustfeed_buffer if weemustfeed_buffer is None: weemustfeed_buffer = weechat.buffer_new( "weemustfeed", "weemustfeed_input_cb", "", "weemustfeed_close_cb", "" ) weechat.buffer_set(weemustfeed_buffer, "title", "WeeMustFeed - a: Add feed, d: Delete feed, u: Update URL, l: List feeds, t: Toggle feed, ?: Show help") set_timer() weechat.buffer_set(weemustfeed_buffer, "display", "1") # switch to it return weechat.WEECHAT_RC_OK def weemustfeed_reset_timer_cb(data, option, value): if weemustfeed_timer is not None: unset_timer() set_timer() return weechat.WEECHAT_RC_OK def weemustfeed_update_single_feed_cb(feed, command, return_code, out, err): global partial_feeds, updating if not feed in partial_feeds: partial_feeds[feed] = "" if return_code == weechat.WEECHAT_HOOK_PROCESS_RUNNING: # feed not done yet partial_feeds[feed] += out return weechat.WEECHAT_RC_OK elif return_code == weechat.WEECHAT_HOOK_PROCESS_ERROR: weechat.prnt_date_tags(weemustfeed_buffer, 0, "irc_error,notify_message", weechat.prefix("error") + "Hook process error for feed '" + feed + "'. === " + command + " === " + out + " === " + err) status = weechat.WEECHAT_RC_ERROR elif return_code == 1: weechat.prnt_date_tags(weemustfeed_buffer, 0, "irc_error,notify_message", weechat.prefix("error") + "Invalid URL for feed '" + feed + "'.") status = weechat.WEECHAT_RC_ERROR elif return_code == 2: weechat.prnt_date_tags(weemustfeed_buffer, 0, "irc_error,notify_message", weechat.prefix("error") + "Transfer error while fetching feed '" + feed + "'.") status = weechat.WEECHAT_RC_ERROR elif return_code == 3: weechat.prnt_date_tags(weemustfeed_buffer, 0, "irc_error,notify_message", weechat.prefix("error") + "Out of memory while fetching feed '" + feed + "'.") status = weechat.WEECHAT_RC_ERROR elif return_code == 4: weechat.prnt_date_tags(weemustfeed_buffer, 0, "irc_error,notify_message", weechat.prefix("error") + "Error with a file while fetching feed '" + feed + "'.") status = weechat.WEECHAT_RC_ERROR elif return_code == 0: # all good, and we have a complete feed if not weechat.config_is_set_plugin("feed." + feed.lower() + ".last_id"): weechat.config_set_plugin("feed." + feed.lower() + ".last_id", "") last_id = "" last_id = weechat.config_get_plugin("feed." + feed.lower() + ".last_id") parsed_feed = feedparser.parse(partial_feeds[feed] + out) entries = list(reversed(parsed_feed.entries)) for entry in entries: if not hasattr(entry, "id"): entry.id = entry.link if (last_id == "") and len(entries) > 0: last_id = entries[-1].id else: if last_id in [entry.id for entry in entries]: only_new = False else: only_new = True for entry in entries: if PY2: entrytitle = entry.title.encode("utf-8") entryurl = entry.link.encode("utf-8") else: entrytitle = entry.title entryurl = entry.link if only_new: weechat.prnt_date_tags(weemustfeed_buffer, 0, "notify_message", "{feed}\t{title} {url}".format(**{ "feed": feed, "title": entrytitle, "url": entryurl })) last_id = entry.id elif entry.id == last_id: only_new = True # everything else will be newer weechat.config_set_plugin("feed." + feed.lower() + ".last_id", last_id) status = weechat.WEECHAT_RC_OK else: # Unknown return code. Script must be updated. weechat.prnt_date_tags(weemustfeed_buffer, 0, "irc_error,notify_message", weechat.prefix("error") + "Unknown return code " + return_code + " for feed '" + feed + "'. Script must be updated.") status = weechat.WEECHAT_RC_ERROR partial_feeds[feed] = "" if feed in updating: updating.remove(feed) if feed in fetch_hooks: del fetch_hooks[feed] return status def weemustfeed_update_feeds_cb(data, remaining_calls): global updating for feed in weechat.config_get_plugin("feeds").strip().split(";"): if weechat.config_is_set_plugin("feed." + feed.lower() + ".url"): if not (weechat.config_is_set_plugin("feed." + feed.lower() + ".enabled") and weechat.config_get_plugin("feed." + feed.lower() + ".enabled").lower() != "yes"): updating.add(feed) if not feed in fetch_hooks: fetch_hooks[feed] = weechat.hook_process( "url:" + weechat.config_get_plugin("feed." + feed.lower() + ".url"), 15000, "weemustfeed_update_single_feed_cb", feed ) elif feed != "": weechat.prnt_date_tags(weemustfeed_buffer, 0, "irc_error,notify_message", weechat.prefix("error") + "Feed '" + feed + "' has no URL set.") return weechat.WEECHAT_RC_OK def set_timer(): global weemustfeed_timer try: timer_interval = int(weechat.config_get_plugin("interval")) except ValueError: timer_interval = int(default_settings["interval"]) weemustfeed_timer = weechat.hook_timer( timer_interval * 1000, 0, 0, "weemustfeed_update_feeds_cb", "" ) def unset_timer(): if weemustfeed_timer is not None: weechat.unhook(weemustfeed_timer) def init_script(): global default_settings for option, default_value in list(default_settings.items()): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, default_value) weechat.hook_command( "weemustfeed", "open/switch to weemustfeed buffer", "", "", "", "weemustfeed_command_cb", "" ) weechat.hook_config( "plugins.var.python.weemustfeed.interval", "weemustfeed_reset_timer_cb", "" ) init_script() weechat-scripts/python/automarkbuffer.py0000644000175100017510000001502715112622405017562 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2015 by nils_2 # # mark buffers as read if there is no new message in a specific time range # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # idea by bascht # # 2015-01-25: nils_2, (freenode.#weechat) # 1.0 : initial release try: import weechat import time except Exception: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") quit() SCRIPT_NAME = "automarkbuffer" SCRIPT_AUTHOR = "nils_2 " SCRIPT_VERSION = "1.0" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "mark buffers as read if there is no new message in a specific time range" TIMER = None WEECHAT_VERSION = "" whitelist = [] OPTIONS = { 'whitelist' : ('','comma separated list of buffer to ignore, e.g. freenode.#weechat,freenode.#weechat-de (check name of buffer with : /buffer localvar)'), 'time' : ('3600','time in seconds to mark buffer as read, if there are no new messages'), 'interval' : ('60','how often in seconds to check for messages'), 'clear' : ('all','hotlist = remove buffer from hotlist, unread = set unread marker, all = hotlist & unread'), 'ignore_hidden' : ('on','hidden messages will be ignored (for example "irc_smart_filter" ones)'), 'ignore_query' : ('on','query buffer(s) will be ignored'), } def check_buffer_timer_cb(data, remaining_calls): global WEECHAT_VERSION,whitelist # search for buffers in hotlist ptr_infolist = weechat.infolist_get("hotlist", "", "") while weechat.infolist_next(ptr_infolist): ptr_buffer = weechat.infolist_pointer(ptr_infolist, "buffer_pointer") localvar_name = weechat.buffer_get_string(ptr_buffer, 'localvar_name') # buffer in whitelist? go to next buffer buf_type = weechat.buffer_get_string(ptr_buffer,'localvar_type') # buffer is a query buffer? if OPTIONS['ignore_query'].lower() == 'on' and buf_type == 'private': continue # buffer in whitelist? if localvar_name in whitelist: continue if ptr_buffer: if get_time_from_line(ptr_buffer): if OPTIONS['clear'].lower() == 'hotlist' or OPTIONS['clear'].lower() == 'all': weechat.buffer_set(ptr_buffer, "hotlist", '-1') if OPTIONS['clear'].lower() == 'unread' or OPTIONS['clear'].lower() == 'all': weechat.command(ptr_buffer,"/input set_unread_current_buffer") weechat.infolist_free(ptr_infolist) return weechat.WEECHAT_RC_OK def get_time_from_line(ptr_buffer): lines = weechat.hdata_pointer(weechat.hdata_get('buffer'), ptr_buffer, 'own_lines') if lines: line = weechat.hdata_pointer(weechat.hdata_get('lines'), lines, 'last_line') last_read_line = weechat.hdata_pointer(weechat.hdata_get('lines'), lines, 'last_read_line') # last line already read? while line != last_read_line: hdata_line = weechat.hdata_get('line') hdata_line_data = weechat.hdata_get('line_data') data = weechat.hdata_pointer(hdata_line, line, 'data') date_last_line = weechat.hdata_time(hdata_line_data, data, 'date') displayed = weechat.hdata_char(hdata_line_data, data, 'displayed') # message hidden? if not displayed and OPTIONS['ignore_hidden'].lower() == 'on': prev_line = weechat.hdata_pointer(hdata_line, line, 'prev_line') line = prev_line continue # buffer empty? if not date_last_line: return 0 get_current_ticks = time.time() time_gone = get_current_ticks - date_last_line if int(OPTIONS['time']) < time_gone: return 1 else: return 0 return 0 # ================================[ weechat options and description ]=============================== def set_timer(): global TIMER if TIMER: weechat.unhook(TIMER) if int(OPTIONS['interval']) == 0: return TIMER = weechat.hook_timer(int(OPTIONS['interval']) * 1000, 0, 0, 'check_buffer_timer_cb', '') def init_options(): for option,value in OPTIONS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) OPTIONS[option] = value[0] else: OPTIONS[option] = weechat.config_get_plugin(option) weechat.config_set_desc_plugin(option, "%s (default: '%s')" % (value[1], value[0])) def toggle_refresh(pointer, name, value): global OPTIONS,whitelist option = name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname OPTIONS[option] = value # save new value if name.endswith('.interval'): # timer value changed? set_timer() if name.endswith('.whitelist'): # whitelist changed? whitelist = OPTIONS['whitelist'].split(',') return weechat.WEECHAT_RC_OK # ================================[ main ]=============================== if __name__ == "__main__": if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): WEECHAT_VERSION = weechat.info_get("version_number", "") or 0 if int(WEECHAT_VERSION) >= 0x01000000: weechat.hook_config( 'plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) init_options() set_timer() whitelist = OPTIONS['whitelist'].split(',') else: weechat.prnt('','%s%s %s' % (weechat.prefix('error'),SCRIPT_NAME,': needs version 1.0 or higher')) weechat-scripts/python/twitch.py0000644000175100017510000007177715112622406016066 0ustar manumanu# SPDX-FileCopyrightText: 2014-2025 mumixam # # SPDX-License-Identifier: GPL-3.0-or-later # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # This script checks stream status of any channel on any servers # listed in the "plugins.var.python.twitch.servers" setting. When you # switch to a buffer it will display updated infomation about the stream # in the title bar. Typing '/twitch' in buffer will also fetch updated # infomation. '/whois nick' will lookup user info and display it in current # buffer. # # https://github.com/mumixam/weechat-twitch # # settings: # plugins.var.python.twitch.servers (default: twitch) # plugins.var.python.twitch.prefix_nicks (default: 1) # plugins.var.python.twitch.debug (default: 0) # plugins.var.python.twitch.ssl_verify (default: 1) # plugins.var.python.twitch.notice_notify_block (default: 1) # plugins.var.python.twitch.client_id (default: awtv6n371jb7uayyc4jaljochyjbfxs) # plugins.var.python.twitch.token (default: "") # # # History: # 2025-09-23, # v1.1: changed hook_modifier from "irc_in_WHISPER" to "irc_in2_WHISPER" -mumixam # changed hook_process_hashtable("url:") to hook_url -mumixam # use rawstrings to keep invalid escapes from throwing warnings -zer0def # # 2024-06-29, mumixam + stacyharper # v1.0: eval client_id and token expressions so that /secure can be used # # 2020-07-27, # v0.9: added support for Oauth token to support twitch APIs requirement -mumixam # fix bug for when api returns null for game_id -mas90 # # 2019-10-13, mumixam # v0.8: changed input modifier hooks to use irc_in2_* instead # added setting 'plugins.var.python.twitch.notice_notify_block' # added setting 'plugins.var.python.twitch.client_id' # # 2019-09-21, mumixam # v0.7: updated script to use current api # 2019-03-03, # v0.6: added support for CLEARMSG -MentalFS # fixed issue with /whois -mumixam # 2018-06-03, mumixam # v0.5: enable curl verbose mode when debug is active, add option to disable ssl/tls verification, # if stream title contains newline char replace it with space # 2017-11-02, mumixam # v0.4: added debug mode for API calls, minor bugfixes # 2017-06-10, mumixam # v0.3: fixed whois output of utf8 display names # 2016-11-03, mumixam # v0.2: added detailed /help # 2016-10-30, mumixam # v0.1: script added to weechat.org SCRIPT_NAME = "twitch" SCRIPT_AUTHOR = "mumixam" SCRIPT_VERSION = "1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "twitch.tv Chat Integration" OPTIONS={ 'servers': ('twitch','Name of server(s) which script will be active on, space seperated'), 'prefix_nicks': ('1','Prefix nicks based on ircv3 tags for mods/subs, This can be cpu intensive on very active chats [1 for enabled, 0 for disabled]'), 'debug': ('0','Debug mode'), 'ssl_verify': ('1', 'Verify SSL/TLS certs'), 'notice_notify_block': ('1', 'Changes notify level of NOTICEs to low'), 'client_id': ('awtv6n371jb7uayyc4jaljochyjbfxs', 'Twitch App ClientID'), 'token': ('', 'Twitch User Token') } import weechat import json from calendar import timegm from datetime import datetime, timedelta import time import string import ast import re curlopt = { "httpheader": "\n".join([ "Authorization: Bearer "+OPTIONS['token'][0], "Client-ID: "+OPTIONS['client_id'][0], ]), "timeout": "5", "verbose": "0", "ssl_verifypeer": "1", "ssl_verifyhost": "2" } gameid_cache = {} uid_cache = {} def days_hours_minutes(td): age = '' hours = td.seconds // 3600 min = td.seconds // 60 % 60 if not td.days == 0: age += str(td.days) + 'd ' if not hours == 0: age += str(hours) + 'h ' if not min == 0: age += str(min) + 'm' return age.strip() def twitch_main(data, buffer, args): if not args == 'bs': weechat.buffer_set(buffer, 'localvar_set_tstatus', '') username = weechat.buffer_get_string(buffer, 'short_name').replace('#', '') server = weechat.buffer_get_string(buffer, 'localvar_server') type = weechat.buffer_get_string(buffer, 'localvar_type') if not (server in OPTIONS['servers'].split() and type == 'channel'): return weechat.WEECHAT_RC_OK url = 'https://api.twitch.tv/helix/streams?user_login=' + username weechat.hook_url(url, curlopt, 7 * 1000, "stream_api", buffer) return weechat.WEECHAT_RC_OK def makeutf8(data): data = data.encode('utf8') if not isinstance(data, str): data=str(data,'utf8') return data def stream_api(data, url, options, output): try: jsonDict = json.loads(output['output'].strip()) except Exception as e: weechat.prnt(data, '%stwitch.py: error communicating with twitch api' % weechat.prefix('error')) if OPTIONS['debug']: weechat.prnt(data,'%stwitch.py: response code: %s' % (weechat.prefix('error'),output['response_code'])) weechat.prnt(data,'%stwitch.py: headers: %s' % (weechat.prefix('error'),output['headers'])) weechat.prnt(data,'%stwitch.py: output: %s' % (weechat.prefix('error'),output['output'])) return weechat.WEECHAT_RC_OK currentbuf = weechat.current_buffer() title_fg = weechat.color( weechat.config_color(weechat.config_get("weechat.bar.title.color_fg"))) title_bg = weechat.color( weechat.config_color(weechat.config_get("weechat.bar.title.color_bg"))) pcolor = weechat.color('chat_prefix_network') ccolor = weechat.color('chat') red = weechat.color('red') blue = weechat.color('blue') green = weechat.color('green') ptime = time.strftime("%H:%M:%S") subs = weechat.buffer_get_string(data, 'localvar_subs') r9k = weechat.buffer_get_string(data, 'localvar_r9k') slow = weechat.buffer_get_string(data, 'localvar_slow') emote = weechat.buffer_get_string(data, 'localvar_emote') if not 'data' in jsonDict.keys(): weechat.prnt(data, 'twitch.py: Error with twitch API (data key missing from json)') if OPTIONS['debug']: weechat.prnt(data, 'twitch.py: %s' % output['output'].strip()) return weechat.WEECHAT_RC_OK if not jsonDict['data']: line = "STREAM: %sOFFLINE%s %sCHECKED AT: (%s)" % ( red, title_fg, blue, ptime) if subs: line += " %s[SUBS]" % title_fg if r9k: line += " %s[R9K]" % title_fg if slow: line += " %s[SLOW@%s]" % (title_fg, slow) if emote: line += " %s[EMOTE]" % title_fg weechat.buffer_set(data, "title", line) else: currenttime = time.time() if len(jsonDict['data']) == 1: jsonDict['data'] = jsonDict['data'][0] output = 'STREAM: %sLIVE%s' % (green, title_fg) if 'game_id' in jsonDict['data']: if jsonDict['data']['game_id']: game = jsonDict['data']['game_id'] game_id = game if game in gameid_cache: game = gameid_cache[game] output += ' <%s> with' % game else: game_id = None else: game_id = None if 'viewer_count' in jsonDict['data']: viewers = jsonDict['data']['viewer_count'] output += ' %s viewers started' % viewers if 'started_at' in jsonDict['data']: createtime = jsonDict['data']['started_at'].replace('Z', 'GMT') starttime = timegm( time.strptime(createtime, '%Y-%m-%dT%H:%M:%S%Z')) dur = timedelta(seconds=currenttime - starttime) uptime = days_hours_minutes(dur) output += ' %s ago' % uptime if 'title' in jsonDict['data']: titleutf8=jsonDict['data']['title'].replace('\n',' ').encode('utf8') titleascii=jsonDict['data']['title'].encode('ascii','replace') if not isinstance(titleutf8, str): titleascii=str(titleascii,'utf8') titleutf8=str(titleutf8,'utf8') oldtitle = weechat.buffer_get_string(data, 'localvar_tstatus') if not oldtitle == titleascii: weechat.prnt(data, '%s--%s Title is "%s"' % (pcolor, ccolor, titleutf8)) weechat.buffer_set(data, 'localvar_set_tstatus', titleascii) output += ' (%s)' % ptime if subs: output += " %s[SUBS]" % title_fg if r9k: output += " %s[R9K]" % title_fg if slow: output += " %s[SLOW@%s]" % (title_fg, slow) if emote: output += " %s[EMOTE]" % title_fg weechat.buffer_set(data, "title", output) if game_id is not None and not game_id in gameid_cache: url = 'https://api.twitch.tv/helix/games?id=' + game_id weechat.hook_url(url, curlopt, 7 * 1000, "game_api", data) return weechat.WEECHAT_RC_OK def game_api(data, url, options, output): try: jsonDict = json.loads(output['output'].strip()) except Exception as e: weechat.prnt(data, '%stwitch.py: error communicating with twitch api' % weechat.prefix('error')) if OPTIONS['debug']: weechat.prnt(data,'%stwitch.py: response code: %s' % (weechat.prefix('error'),output['response_code'])) weechat.prnt(data,'%stwitch.py: headers: %s' % (weechat.prefix('error'),output['headers'])) weechat.prnt(data,'%stwitch.py: output: %s' % (weechat.prefix('error'),output['output'])) return weechat.WEECHAT_RC_OK if 'data' in jsonDict.keys(): if not jsonDict['data']: return weechat.WEECHAT_RC_OK if len(jsonDict['data']) == 1: jsonDict['data'] = jsonDict['data'][0] old_title = weechat.buffer_get_string(data, "title") id = jsonDict['data']['id'] name = makeutf8(jsonDict['data']['name']) new_title = old_title.replace('<{}>'.format(id),'<{}>'.format(name)) weechat.buffer_set(data, "title", new_title) gameid_cache[id] = name return weechat.WEECHAT_RC_OK def channel_api(data, url, options, output): try: jsonDict = json.loads(output['output'].strip()) except Exception as e: weechat.prnt(data, '%stwitch.py: error communicating with twitch api' % weechat.prefix('error')) if OPTIONS['debug']: weechat.prnt(data,'%stwitch.py: response code: %s' % (weechat.prefix('error'),output['response_code'])) weechat.prnt(data,'%stwitch.py: headers: %s' % (weechat.prefix('error'),output['headers'])) weechat.prnt(data,'%stwitch.py: output: %s' % (weechat.prefix('error'),output['output'])) return weechat.WEECHAT_RC_OK currentbuf = weechat.current_buffer() pcolor = weechat.color('chat_prefix_network') ccolor = weechat.color('chat') dcolor = weechat.color('chat_delimiters') ncolor = weechat.color('chat_nick') ul = weechat.color("underline") rul = weechat.color("-underline") pformat = weechat.config_string( weechat.config_get("weechat.look.prefix_network")) if 'total' in jsonDict: uid = command.split('=')[-1] name = 'WHOIS' if 'to_id' in command: followers = jsonDict['total'] if uid in uid_cache: name = uid_cache[uid] output = '%s%s %s[%s%s%s]%s %sFollowers%s: %s' % ( pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, followers) weechat.prnt(data, makeutf8(output)) url = 'https://api.twitch.tv/helix/users/follows?from_id=' + uid url_hook = weechat.hook_url(url, curlopt, 7 * 1000, "channel_api", data) return weechat.WEECHAT_RC_OK if 'from_id' in command: following = jsonDict['total'] if uid in uid_cache: name = uid_cache[uid] output = '%s%s %s[%s%s%s]%s %sFollowing%s: %s' % ( pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, following) weechat.prnt(data, makeutf8(output)) return weechat.WEECHAT_RC_OK if ('users' in jsonDict) and jsonDict['users'] and len(jsonDict['users'][0]) == 8: dname = jsonDict['users'][0]['display_name'] name = jsonDict['users'][0]['name'] create = jsonDict['users'][0]['created_at'].split('T')[0] status = jsonDict['users'][0]['bio'] uid = jsonDict['users'][0]['_id'] uid_cache[uid] = name output = '%s%s %s[%s%s%s]%s %sDisplay Name%s: %s' % ( pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, dname) output += '\n%s%s %s[%s%s%s]%s %sAccount Created%s: %s' % ( pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, create) if status: output += '\n%s%s %s[%s%s%s]%s %sBio%s: %s' % ( pcolor, pformat, dcolor, ncolor, name, dcolor, ccolor, ul, rul, status) weechat.prnt(data, makeutf8(output)) url = 'https://api.twitch.tv/helix/users/follows?to_id=' + uid url_hook = weechat.hook_url(url, curlopt, 7 * 1000, "channel_api", data) else: weechat.prnt(data, 'Error: No Such User') return weechat.WEECHAT_RC_OK def twitch_clearchat(data, modifier, modifier_data, string): mp = weechat.info_get_hashtable( 'irc_message_parse', {"message": string}) server = modifier_data user = mp['text'] channel = mp['channel'] try: tags = dict([s.split('=',1) for s in mp['tags'].split(';')]) except: tags = '' buffer = weechat.buffer_search("irc", "%s.%s" % (server, channel)) if buffer: pcolor = weechat.color('chat_prefix_network') ccolor = weechat.color('chat') ul = weechat.color("underline") rul = weechat.color("-underline") if user: if 'ban-duration' in tags: if 'ban-reason' in tags and tags['ban-reason']: bn=re.sub(r'\s',' ', tags['ban-reason']) weechat.prnt(buffer,"%s--%s %s has been timed out for %s seconds %sReason%s: %s" % (pcolor, ccolor, user, tags['ban-duration'], ul, rul, bn)) else: weechat.prnt(buffer,"%s--%s %s has been timed out for %s seconds" % (pcolor, ccolor, user, tags['ban-duration'])) elif 'ban-reason' in tags: if tags['ban-reason']: bn=re.sub(r'\s', ' ', tags['ban-reason']) weechat.prnt(buffer,"%s--%s %s has been banned %sReason%s: %s" % (pcolor, ccolor, user, ul, rul,bn)) else: weechat.prnt(buffer,"%s--%s %s has been banned" % (pcolor, ccolor, user)) else: weechat.prnt( buffer, "%s--%s %s's Chat Cleared By Moderator" % (pcolor, ccolor, user)) else: weechat.prnt( buffer, "%s--%s Entire Chat Cleared By Moderator" % (pcolor, ccolor)) return "" def twitch_clearmsg(data, modifier, modifier_data, string): mp = weechat.info_get_hashtable( 'irc_message_parse', {"message": string}) server = modifier_data channel = mp['channel'] try: tags = dict([s.split('=',1) for s in mp['tags'].split(';')]) except: tags = '' buffer = weechat.buffer_search("irc", "%s.%s" % (server, channel)) if buffer: pcolor = weechat.color('chat_prefix_network') ccolor = weechat.color('chat') if 'login' in tags: weechat.prnt(buffer,"%s--%s a message from %s was deleted" % (pcolor, ccolor, tags['login'])) else: weechat.prnt(buffer, "%s--%s a message was deleted" % (pcolor, ccolor)) return "" def twitch_suppress(data, modifier, modifier_data, string): return "" def twitch_reconnect(data, modifier, modifier_data, string): server = modifier_data buffer = weechat.buffer_search("irc", "server.%s" % server) if buffer: pcolor = weechat.color('chat_prefix_network') ccolor = weechat.color('chat') weechat.prnt( buffer, "%s--%s Server sent reconnect request. Issuing /reconnect" % (pcolor, ccolor)) weechat.command(buffer, "/reconnect") return "" def twitch_buffer_switch(data, signal, signal_data): server = weechat.buffer_get_string(signal_data, 'localvar_server') type = weechat.buffer_get_string(signal_data, 'localvar_type') if not (server in OPTIONS['servers'].split() and type == 'channel'): return weechat.WEECHAT_RC_OK twitch_main('', signal_data, 'bs') return weechat.WEECHAT_RC_OK def twitch_roomstate(data, modifier, server, string): message = weechat.info_get_hashtable( 'irc_message_parse', {"message": string}) buffer = weechat.buffer_search( "irc", "%s.%s" % (server, message['channel'])) for tag in message['tags'].split(';'): if tag == 'subs-only=0': weechat.buffer_set(buffer, 'localvar_set_subs', '') if tag == 'subs-only=1': weechat.buffer_set(buffer, 'localvar_set_subs', '1') if tag == 'r9k=0': weechat.buffer_set(buffer, 'localvar_set_r9k', '') if tag == 'r9k=1': weechat.buffer_set(buffer, 'localvar_set_r9k', '1') if tag == 'emote-only=0': weechat.buffer_set(buffer, 'localvar_set_emote', '') if tag == 'emote-only=1': weechat.buffer_set(buffer, 'localvar_set_emote', '1') if tag.startswith('slow='): value = tag.split('=')[-1] if value == '0': weechat.buffer_set(buffer, 'localvar_set_slow', '') if value > '0': weechat.buffer_set(buffer, 'localvar_set_slow', value) twitch_main('', buffer, 'bs') return '' def twitch_usernotice(data, modifier, server, string): pcolor = weechat.color('chat_prefix_network') ccolor = weechat.color('chat') mp = weechat.info_get_hashtable( 'irc_message_parse', {"message": string}) buffer = weechat.buffer_search( "irc", "%s.%s" % (server, mp['channel'])) if mp['tags']: tags = dict([s.split('=',1) for s in mp['tags'].split(';')]) msg = re.sub(r'\\s', ' ', tags['system-msg']) if mp['text']: msg += ' [Comment] '+mp['text'] weechat.prnt(buffer, '%s--%s %s' % (pcolor, ccolor, msg)) return '' def twitch_whisper(data, modifier, modifier_data, string): message = weechat.info_get_hashtable( 'irc_message_parse', {"message": string}) if message['tags']: string = '@'+message['tags']+' ' else: string = '' string += ':'+message['host'] string += ' PRIVMSG' string += ' '+message['arguments'] return string def twitch_privmsg(data, modifier, server_name, string): if not server_name in OPTIONS['servers'].split(): return string message = weechat.info_get_hashtable( 'irc_message_parse', {"message": string}) if message['channel'].startswith('#'): return string newmsg = 'PRIVMSG #%s :/w %s %s' % (message['nick'],message['nick'],message['text']) return newmsg def twitch_in_privmsg(data, modifier, server_name, string, prefix=''): if not OPTIONS['prefix_nicks']: return string if not server_name in OPTIONS['servers'].split(): return string mp = weechat.info_get_hashtable("irc_message_parse", {"message": string}) if not mp['tags']: return string if not '#' in mp['channel']: return string if '#' + mp['nick'] == mp['channel']: return mp['message_without_tags'].replace(mp['nick'], '~' + mp['nick'], 1) tags = dict([s.split('=',1) for s in mp['tags'].split(';')]) if tags['user-type'] == 'mod': prefix += '@' if tags['subscriber'] == '1': prefix += '%' if prefix: msg = mp['message_without_tags'].replace( mp['nick'], prefix + mp['nick'], 1) return '@' + mp['tags'] + ' ' + msg else: return string def twitch_whois(data, modifier, server_name, string): if not server_name in OPTIONS['servers'].split(): return string msg = weechat.info_get_hashtable("irc_message_parse", {"message": string}) username = msg['nick'].lower() currentbuf = weechat.current_buffer() url = 'https://api.twitch.tv/kraken/users?login=' + username params='&api_version=5' url_hook = weechat.hook_url(url+params, curlopt, 7 * 1000, "channel_api", currentbuf) return "" def twitch_notice(data, line): if not OPTIONS['notice_notify_block']: return string return {"notify_level": "0"} def config_setup(): for option,value in OPTIONS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) weechat.config_set_desc_plugin(option, '%s' % value[1]) OPTIONS[option] = value[0] else: if option == 'prefix_nicks' or option == 'debug' or option == 'ssl_verify' or option == 'notice_notify_block': OPTIONS[option] = weechat.config_string_to_boolean( weechat.config_get_plugin(option)) else: OPTIONS[option] = weechat.config_get_plugin(option) if option == 'debug': curlopt['verbose'] = weechat.config_get_plugin(option) if option == 'ssl_verify': if weechat.config_get_plugin(option) == 0: curlopt['ssl_verifypeer'] = "0" curlopt['ssl_verifyhost'] = "0" else: curlopt['ssl_verifypeer'] = "1" curlopt['ssl_verifyhost'] = "2" if option == 'client_id': hlist = [] cidv = weechat.config_get_plugin(option) tokv = weechat.config_get_plugin('token') if tokv[:6] == "${sec.": tokv = weechat.string_eval_expression(tokv, {}, {}, {}) if cidv: hlist.append('Client-ID: '+cidv) if tokv: hlist.append('Authorization: Bearer '+tokv) if hlist: curlopt['httpheader'] = '\n'.join(hlist) if option == 'token': hlist = [] cidv = weechat.config_get_plugin('client_id') tokv = weechat.config_get_plugin(option) if tokv[:6] == "${sec.": tokv = weechat.string_eval_expression(tokv, {}, {}, {}) if tokv: hlist.append('Authorization: Bearer '+tokv) if cidv: hlist.append('Client-ID: '+cidv) if hlist: curlopt['httpheader'] = '\n'.join(hlist) def config_change(pointer, name, value): option = name.replace('plugins.var.python.'+SCRIPT_NAME+'.','') if option == 'prefix_nicks' or option == 'debug' or option == 'ssl_verify' or option == 'notice_notify_block': value=weechat.config_string_to_boolean(value) if option == 'debug': if value == 0: curlopt['verbose'] = "0" if value == 1: curlopt['verbose'] = "1" if option == 'ssl_verify': if value == 0: curlopt['ssl_verifypeer'] = "0" curlopt['ssl_verifyhost'] = "0" if value == 1: curlopt['ssl_verifypeer'] = "1" curlopt['ssl_verifyhost'] = "2" if option == 'client_id': for x in curlopt['httpheader'].split('\n'): if x.startswith('Authorization: Bearer'): curlopt['httpheader'] = x + '\n' + "Client-ID: " + value break if option == 'token': if value[:6] == "${sec.": value = weechat.string_eval_expression(value, {}, {}, {}) for x in curlopt['httpheader'].split('\n'): if x.startswith('Client-ID:'): curlopt['httpheader'] = x + '\n' + "Authorization: Bearer " + value break OPTIONS[option] = value return weechat.WEECHAT_RC_OK if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): weechat.hook_command("twitch", SCRIPT_DESC, "", " settings:\n" " plugins.var.python.twitch.servers (default: twitch)\n" " plugins.var.python.twitch.prefix_nicks (default: 1)\n" " plugins.var.python.twitch.debug (default: 0)\n" " plugins.var.python.twitch.ssl_verify (default: 0)\n" " plugins.var.python.twitch.notice_notify_block (default: 1)\n" " plugins.var.python.twitch.client_id (default: awtv6n371jb7uayyc4jaljochyjbfxs)\n" "\n\n" " This script checks stream status of any channel on any servers listed\n" " in the \"plugins.var.python.twitch.servers\" setting. When you switch\n" " to a buffer it will display updated infomation about the stream in the\n" " title bar. Typing '/twitch' in a buffer will also fetch updated infomation.\n" " '/whois nick' will lookup user info and display it in current buffer.\n\n" " Option \"plugins.var.python.twitch.servers\" controls\n" " what server this script will work on. The default is twitch\n" " but you can have multiples separated by a space.\n" " /set plugins.var.python.twitch.servers twitch twitchcopy\n" "\n\n" " This script also will prefix users nicks (@ for mod, % for sub,\n" " and ~ for broadcaster). This will break the traditional function\n" " of `/ignore add nightbot` and will require you to prefix nicks if you\n" " want to ignore someone `/ignore add re:[~@%]{0,3}nightbot` should ignore\n" " a nick with all or none of the prefixes used by this script.\n" " NOTE: This may cause high cpu usage in very active chat and/or on slower cpus.\n" " This can also be disabled by setting\n /set plugins.var.python.twitch.prefix_nicks off\n" "\n\n" " If you are experiencing errors you can enable debug mode by setting\n" " /set plugins.var.python.twitch.debug on\n" " You can also try disabling SSL/TLS cert verification.\n" " /set plugins.var.python.twitch.ssl_verify off\n" "\n\n" " Required server settings:\n" " /server add twitch irc.chat.twitch.tv\n" " /set irc.server.twitch.capabilities \"twitch.tv/membership,twitch.tv/commands,twitch.tv/tags\"\n" " /set irc.server.twitch.nicks \"My Twitch Username\"\n" " /set irc.server.twitch.password \"oauth:My Oauth Key\"\n" "\n" " If you do not have a oauth token one can be generated for your account here\n" " https://mumixam.github.io/weechat_twitch\n" "\n" " This script now by default limits the level of NOTICEs from twitch server\n" " What this does is makes it so 'Now hosting' notifications are classes as a low level message\n" " So they no longer show up in your hotlist like a 'actual' message\n" " If you would like to disable this set the following\n" " /set plugins.var.python.twitch.notice_notify_block 0\n" "\n" " If would like to use your own Client-ID it can be set with\n" " /set plugins.var.python.twitch.client_id (clientid)\n" "\n" " Twitch Helix API now requires a OAuth token for any API calls. Your token has the match your ClientID\n" " One can be generated here that matches the default CleintID here:\n" " https://mumixam.github.io/weechat_twitch\n" " /set plugins.var.python.twitch.token (token from url)\n" "\n" " This script also has whisper support that works like a standard query. \"/query user\"\n\n", "", "twitch_main", "") weechat.hook_signal('buffer_switch', 'twitch_buffer_switch', '') weechat.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'config_change', '') config_setup() weechat.hook_line("", "", "irc_notice+nick_tmi.twitch.tv", "twitch_notice", "") weechat.hook_modifier("irc_in2_CLEARCHAT", "twitch_clearchat", "") weechat.hook_modifier("irc_in2_CLEARMSG", "twitch_clearmsg", "") weechat.hook_modifier("irc_in2_RECONNECT", "twitch_reconnect", "") weechat.hook_modifier("irc_in2_USERSTATE", "twitch_suppress", "") weechat.hook_modifier("irc_in2_HOSTTARGET", "twitch_suppress", "") weechat.hook_modifier("irc_in2_ROOMSTATE", "twitch_roomstate", "") weechat.hook_modifier("irc_in2_USERNOTICE", "twitch_usernotice", "") weechat.hook_modifier("irc_in2_WHISPER", "twitch_whisper", "") weechat.hook_modifier("irc_out_PRIVMSG", "twitch_privmsg", "") weechat.hook_modifier("irc_out_WHOIS", "twitch_whois", "") weechat.hook_modifier("irc_in2_PRIVMSG", "twitch_in_privmsg", "") weechat-scripts/python/whois_on_query.py0000644000175100017510000001117015112622375017620 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2009-2012 Sebastien Helleu # Copyright (C) 2011 Elián Hanisch # Copyright (C) 2011 ArZa # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Send "whois" on nick when receiving (or opening) new IRC query. # (this script requires WeeChat 0.3.2 or newer) # # History: # # 2017-05-28, Jos Ahrens : # version 0.6.1: Corrected a typo in help description for option self_query # 2012-01-03, Sebastien Helleu : # version 0.6: make script compatible with Python 3.x # 2011-10-17, Sebastien Helleu : # version 0.5: add option "self_query" to do whois on self query, # add help for options (WeeChat >= 0.3.5) # 2011-07-06, ArZa : # version 0.4: fix target buffer for command # 2011-05-31, Elián Hanisch : # version 0.3: depends on WeeChat 0.3.2 # use irc_is_nick instead of irc_is_channel. # only /whois when somebody opens a query with you. # 2009-05-02, Sebastien Helleu : # version 0.2: sync with last API changes # 2009-02-08, Sebastien Helleu : # version 0.1: initial release # try: import weechat from weechat import WEECHAT_RC_OK, prnt import_ok = True except ImportError: print('This script must be run under WeeChat.') print('Get WeeChat now at: http://www.weechat.org/') import_ok = False SCRIPT_NAME = 'whois_on_query' SCRIPT_AUTHOR = 'Sebastien Helleu ' SCRIPT_VERSION = '0.6.1' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = 'Whois on query' # script options woq_settings_default = { 'command' : ('/whois $nick $nick', 'the command sent to do the whois ($nick is replaced by nick)'), 'self_query': ('off', 'if on, send whois for self queries'), } irc_pv_hook = '' def unhook_irc_pv(): """Remove irc_pv hook.""" global irc_pv_hook if irc_pv_hook: weechat.unhook(irc_pv_hook) irc_pv_hook = '' def exec_command(buffer, nick): """Execute the whois command.""" command = weechat.config_get_plugin('command').replace('$nick', nick) weechat.command(buffer, command) def signal_irc_pv_opened(data, signal, signal_data): """Callback for signal 'irc_pv_opened'.""" global irc_pv_hook if weechat.buffer_get_string(signal_data, 'plugin') == 'irc': nick = weechat.buffer_get_string(signal_data, 'localvar_channel') if weechat.info_get('irc_is_nick', nick) == '1': unhook_irc_pv() if weechat.config_get_plugin('self_query') == 'on': exec_command(signal_data, nick) else: # query open, wait for a msg to come (query was open by user) or if we send a msg out # (query was open by us) server = weechat.buffer_get_string(signal_data, 'localvar_server') irc_pv_hook = weechat.hook_signal('irc_pv', 'signal_irc_pv', '%s,%s' % (signal_data, nick)) return weechat.WEECHAT_RC_OK def signal_irc_pv(data, signal, signal_data): """Callback for signal 'irc_pv'.""" buffer, nick = data.split(',') if signal_data.startswith(':' + nick + '!'): # ok, run command exec_command(buffer, nick) unhook_irc_pv() return weechat.WEECHAT_RC_OK if __name__ == '__main__' and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): # set default settings version = weechat.info_get('version_number', '') or 0 for option, value in woq_settings_default.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) if int(version) >= 0x00030500: weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) # hook signal 'irc_pv_opened' weechat.hook_signal('irc_pv_opened', 'signal_irc_pv_opened', '') weechat-scripts/python/emojize.py0000644000175100017510000000372315112622410016203 0ustar manumanu""" Weechat plugin to convert emoji shortcodes to unicode emoji. This plugin is a thin wrapper around the emoji package for python. It converts emoji shortcodes to Unicode emoji. This package is based on the emoji_aliases.py script by Mike Reinhardt. License: CC0 Author: Thom Wiggers Repository: https://github.com/thomwiggers/weechat-emojize This plugin supports python 3 and requires the 'emoji' python package. Requires at least weechat 1.3 Changelog: 1.0.1 - 2023-08-06: mva Adaptation to modern version of `emoji` package (use_aliases => language="alias") """ def register(): weechat.register( "emojize", "Thom Wiggers", "1.0.1", "CC0", "Convert emoji shortcodes to unicode emoji", "", # shutdown function "utf-8", ) import_ok = True try: import emoji except ImportError: print("Failed to import emoji package, try installing 'emoji'") import_ok = False import weechat HOOKS = ( "away", "cnotice", "cprivmsg", "kick", "knock", "notice", "part", "privmsg", "quit", "wallops", ) def convert_emoji(_data, modifier, _modifier_data, string): """Convert the emoji in event messages""" # Check if this message has a segment we shouldn't touch. msg = weechat.info_get_hashtable("irc_message_parse", {"message": string}) pos_text = int(msg["pos_text"]) if msg["text"] != "" and pos_text > 0: return ( string[:pos_text] + emoji.emojize(msg["text"], language="alias") + string[(pos_text + len(msg["text"])):] ) if modifier == "input_text_for_buffer": return emoji.emojize(string, language="alias") return string if __name__ == "__main__" and import_ok: register() weechat.hook_modifier("input_text_for_buffer", "convert_emoji", "") for hook in HOOKS: weechat.hook_modifier("irc_in2_{}".format(hook), "convert_emoji", "") weechat-scripts/python/btc_ticker.py0000644000175100017510000001321315112622404016650 0ustar manumanu# SPDX-FileCopyrightText: 2014-2018 Eugene Ciurana (pr3d4t0r) # # SPDX-License-Identifier: BSD-3-Clause # # Version - see _VERSION global # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, this # list of conditions and the following disclaimer in the documentation and/or # other materials provided with the distribution. # # * Neither the name of the {organization} nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Main repository, version history: https://github.com/pr3d4t0r/weechat-btc-ticker # # Version history: https://github.com/pr3d4t0r/weechat-btc-ticker from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals from datetime import datetime import json import weechat # *** constants *** CRYPTOCUR_API_TIME_OUT = 15000 # ms CRYPTOCUR_API_URI = 'url:https://api.cryptonator.com/api/ticker/%s-%s' DEFAULT_CRYPTO_CURRENCY = 'btc' DEFAULT_FIAT_CURRENCY = 'usd' VALID_CRYPTO_CURRENCIES = [ DEFAULT_CRYPTO_CURRENCY, 'eth', 'bch', 'xrp', 'xem', 'ltc', 'dash', 'neo', 'etc', ] VALID_FIAT_CURRENCIES = [ DEFAULT_FIAT_CURRENCY, 'eur', 'rur', ] _VERSION = '2.1.1' COMMAND_NICK = 'tick' # *** Functions *** def extractRelevantInfoFrom(rawTicker): payload = json.loads(rawTicker) result = payload['ticker'] return result def display(buffer, ticker): baseCurrency = ticker['base'] targetCurrency = ticker['target'] price = float(ticker['price']) volume = float(ticker['volume']) change = float(ticker['change']) now = datetime.now().strftime('%Y-%m-%dT%H:%M:%S') output = '%s:%s price = %5.2f, volume = %5.2f, change = %4.2f on %s' % ( baseCurrency, targetCurrency, price, volume, change, now) weechat.command(buffer, '/say %s' % output) def displayCurrentTicker(buffer, rawTicker): if rawTicker: ticker = extractRelevantInfoFrom(rawTicker) display(buffer, ticker) else: weechat.prnt(buffer, '%s\t*** UNABLE TO READ DATA FROM: %s ***' % (COMMAND_NICK, CRYPTOCUR_API_URI)) def tickerPayloadHandler(_, service, returnCode, out, err): if returnCode == weechat.WEECHAT_HOOK_PROCESS_ERROR: weechat.prnt("", "%s\tError with service call '%s'" % (COMMAND_NICK, service)) return weechat.WEECHAT_RC_OK displayCurrentTicker('', out) return weechat.WEECHAT_RC_OK def fetchJSONTickerFor(cryptoCurrency, fiatCurrency): serviceURI = CRYPTOCUR_API_URI % (cryptoCurrency, fiatCurrency) weechat.hook_process(serviceURI, CRYPTOCUR_API_TIME_OUT, 'tickerPayloadHandler', "") def displayCryptoCurrencyTicker(data, buffer, arguments): cryptoCurrency = DEFAULT_CRYPTO_CURRENCY fiatCurrency = DEFAULT_FIAT_CURRENCY if len(arguments): tickerArguments = arguments.split(' ') # no argparse module; these aren't CLI, but WeeChat's arguments if len(tickerArguments) >= 1: if tickerArguments[0].lower() in VALID_CRYPTO_CURRENCIES: cryptoCurrency = tickerArguments[0].lower() else: weechat.prnt(buffer, '%s\tInvalid crypto currency; using default %s' % (COMMAND_NICK, DEFAULT_CRYPTO_CURRENCY)) if len(tickerArguments) == 2: if tickerArguments[1].lower() in VALID_FIAT_CURRENCIES: fiatCurrency = tickerArguments[1].lower() else: weechat.prnt(buffer, '%s\tInvalid fiat currency; using default %s' % (COMMAND_NICK, DEFAULT_FIAT_CURRENCY)) fetchJSONTickerFor(cryptoCurrency, fiatCurrency) return weechat.WEECHAT_RC_OK # *** main *** weechat.register('btc_ticker', 'pr3d4t0r', _VERSION, 'BSD', 'Display a crypto currency spot price ticker (BTC, ETH, LTC) in the active buffer', '', 'UTF-8') cryptoCurrencies = '|'.join(sorted(VALID_CRYPTO_CURRENCIES)) fiatCurrencies = '|'.join(VALID_FIAT_CURRENCIES) argsWeeChat = '[%s [%s] ]' % (cryptoCurrencies, fiatCurrencies) weechat.hook_command(COMMAND_NICK, 'Display common crypto currency spot exchange values conveted to fiat currencies like USD or EUR',\ argsWeeChat, ' btc = Bitcoin\n eth = Ethereum\n bch = Bitcoin Cash\n xrp = Ripple\n xem = NEM\n ltc = Litecoin\n dash = Dash\n neo = NEO\n etc = Ethereum Classic\n\n usd = US dollar\n eur = euro\n rur = Russian ruble', '', 'displayCryptoCurrencyTicker', '') weechat-scripts/python/anotify.py0000644000175100017510000003732015112622403016214 0ustar manumanu# -*- coding: utf-8 -*- # # anotify.py # Copyright (c) 2012 magnific0 # # based on: # growl.py # Copyright (c) 2011 Sorin Ionescu # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. SCRIPT_NAME = 'anotify' SCRIPT_AUTHOR = 'magnific0' SCRIPT_VERSION = '1.0.2' SCRIPT_LICENSE = 'MIT' SCRIPT_DESC = 'Sends libnotify notifications upon events.' # Changelog # 2014-05-10: v1.0.1 Change hook_print callback argument type of # displayed/highlight (WeeChat >= 1.0) # 2012-09-20: v1.0.0 Forked from original and adapted for libnotify. # ----------------------------------------------------------------------------- # Settings # ----------------------------------------------------------------------------- SETTINGS = { 'show_public_message': 'off', 'show_private_message': 'on', 'show_public_action_message': 'off', 'show_private_action_message': 'on', 'show_notice_message': 'off', 'show_invite_message': 'on', 'show_highlighted_message': 'on', 'show_server': 'on', 'show_channel_topic': 'on', 'show_dcc': 'on', 'show_upgrade_ended': 'on', 'sticky': 'off', 'sticky_away': 'on', 'icon': '/usr/share/pixmaps/weechat.xpm', } # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- try: import re import os import weechat import notify2 IMPORT_OK = True except ImportError as error: IMPORT_OK = False if str(error).find('weechat') != -1: print('This script must be run under WeeChat.') print('Get WeeChat at http://www.weechat.org.') else: weechat.prnt('', 'anotify: {0}'.format(error)) # ----------------------------------------------------------------------------- # Globals # ----------------------------------------------------------------------------- TAGGED_MESSAGES = { 'public message or action': set(['irc_privmsg', 'notify_message']), 'private message or action': set(['irc_privmsg', 'notify_private']), 'notice message': set(['irc_notice', 'notify_private']), 'invite message': set(['irc_invite', 'notify_highlight']), 'channel topic': set(['irc_topic', ]), #'away status': set(['away_info', ]), } UNTAGGED_MESSAGES = { 'away status': re.compile(r'^You ((\w+).){2,3}marked as being away', re.UNICODE), 'dcc chat request': re.compile(r'^xfer: incoming chat request from (\w+)', re.UNICODE), 'dcc chat closed': re.compile(r'^xfer: chat closed with (\w+)', re.UNICODE), 'dcc get request': re.compile( r'^xfer: incoming file from (\w+) [^:]+: ((?:,\w|[^,])+),', re.UNICODE), 'dcc get completed': re.compile(r'^xfer: file ([^\s]+) received from \w+: OK', re.UNICODE), 'dcc get failed': re.compile( r'^xfer: file ([^\s]+) received from \w+: FAILED', re.UNICODE), 'dcc send completed': re.compile(r'^xfer: file ([^\s]+) sent to \w+: OK', re.UNICODE), 'dcc send failed': re.compile(r'^xfer: file ([^\s]+) sent to \w+: FAILED', re.UNICODE), } DISPATCH_TABLE = { 'away status': 'set_away_status', 'public message or action': 'notify_public_message_or_action', 'private message or action': 'notify_private_message_or_action', 'notice message': 'notify_notice_message', 'invite message': 'notify_invite_message', 'channel topic': 'notify_channel_topic', 'dcc chat request': 'notify_dcc_chat_request', 'dcc chat closed': 'notify_dcc_chat_closed', 'dcc get request': 'notify_dcc_get_request', 'dcc get completed': 'notify_dcc_get_completed', 'dcc get failed': 'notify_dcc_get_failed', 'dcc send completed': 'notify_dcc_send_completed', 'dcc send failed': 'notify_dcc_send_failed', } STATE = { 'icon': None, 'is_away': False } # ----------------------------------------------------------------------------- # Notifiers # ----------------------------------------------------------------------------- def cb_irc_server_connected(data, signal, signal_data): '''Notify when connected to IRC server.''' if weechat.config_get_plugin('show_server') == 'on': a_notify( 'Server', 'Server Connected', 'Connected to network {0}.'.format(signal_data)) return weechat.WEECHAT_RC_OK def cb_irc_server_disconnected(data, signal, signal_data): '''Notify when disconnected to IRC server.''' if weechat.config_get_plugin('show_server') == 'on': a_notify( 'Server', 'Server Disconnected', 'Disconnected from network {0}.'.format(signal_data)) return weechat.WEECHAT_RC_OK def cb_notify_upgrade_ended(data, signal, signal_data): '''Notify on end of WeeChat upgrade.''' if weechat.config_get_plugin('show_upgrade_ended') == 'on': a_notify( 'WeeChat', 'WeeChat Upgraded', 'WeeChat has been upgraded.') return weechat.WEECHAT_RC_OK def notify_highlighted_message(prefix, message): '''Notify on highlighted message.''' if weechat.config_get_plugin("show_highlighted_message") == "on": a_notify( 'Highlight', 'Highlighted Message', "{0}: {1}".format(prefix, message), priority=notify2.URGENCY_CRITICAL) def notify_public_message_or_action(prefix, message, highlighted): '''Notify on public message or action.''' if prefix == ' *': regex = re.compile(r'^(\w+) (.+)$', re.UNICODE) match = regex.match(message) if match: prefix = match.group(1) message = match.group(2) notify_public_action_message(prefix, message, highlighted) else: if highlighted: notify_highlighted_message(prefix, message) elif weechat.config_get_plugin("show_public_message") == "on": a_notify( 'Public', 'Public Message', '{0}: {1}'.format(prefix, message)) def notify_private_message_or_action(prefix, message, highlighted): '''Notify on private message or action.''' regex = re.compile(r'^CTCP_MESSAGE.+?ACTION (.+)$', re.UNICODE) match = regex.match(message) if match: notify_private_action_message(prefix, match.group(1), highlighted) else: if prefix == ' *': regex = re.compile(r'^(\w+) (.+)$', re.UNICODE) match = regex.match(message) if match: prefix = match.group(1) message = match.group(2) notify_private_action_message(prefix, message, highlighted) else: if highlighted: notify_highlighted_message(prefix, message) elif weechat.config_get_plugin("show_private_message") == "on": a_notify( 'Private', 'Private Message', '{0}: {1}'.format(prefix, message)) def notify_public_action_message(prefix, message, highlighted): '''Notify on public action message.''' if highlighted: notify_highlighted_message(prefix, message) elif weechat.config_get_plugin("show_public_action_message") == "on": a_notify( 'Action', 'Public Action Message', '{0}: {1}'.format(prefix, message), priority=notify2.URGENCY_NORMAL) def notify_private_action_message(prefix, message, highlighted): '''Notify on private action message.''' if highlighted: notify_highlighted_message(prefix, message) elif weechat.config_get_plugin("show_private_action_message") == "on": a_notify( 'Action', 'Private Action Message', '{0}: {1}'.format(prefix, message), priority=notify2.URGENCY_NORMAL) def notify_notice_message(prefix, message, highlighted): '''Notify on notice message.''' regex = re.compile(r'^([^\s]*) [^:]*: (.+)$', re.UNICODE) match = regex.match(message) if match: prefix = match.group(1) message = match.group(2) if highlighted: notify_highlighted_message(prefix, message) elif weechat.config_get_plugin("show_notice_message") == "on": a_notify( 'Notice', 'Notice Message', '{0}: {1}'.format(prefix, message)) def notify_invite_message(prefix, message, highlighted): '''Notify on channel invitation message.''' if weechat.config_get_plugin("show_invite_message") == "on": regex = re.compile( r'^You have been invited to ([^\s]+) by ([^\s]+)$', re.UNICODE) match = regex.match(message) if match: channel = match.group(1) nick = match.group(2) a_notify( 'Invite', 'Channel Invitation', '{0} has invited you to join {1}.'.format(nick, channel)) def notify_channel_topic(prefix, message, highlighted): '''Notify on channel topic change.''' if weechat.config_get_plugin("show_channel_topic") == "on": regex = re.compile( r'^\w+ has (?:changed|unset) topic for ([^\s]+)' + '(?:(?: from "(?:(?:"\w|[^"])+)")? to "((?:"\w|[^"])+)")?', re.UNICODE) match = regex.match(message) if match: channel = match.group(1) topic = match.group(2) or '' a_notify( 'Channel', 'Channel Topic', "{0}: {1}".format(channel, topic)) def notify_dcc_chat_request(match): '''Notify on DCC chat request.''' if weechat.config_get_plugin("show_dcc") == "on": nick = match.group(1) a_notify( 'DCC', 'Direct Chat Request', '{0} wants to chat directly.'.format(nick)) def notify_dcc_chat_closed(match): '''Notify on DCC chat termination.''' if weechat.config_get_plugin("show_dcc") == "on": nick = match.group(1) a_notify( 'DCC', 'Direct Chat Ended', 'Direct chat with {0} has ended.'.format(nick)) def notify_dcc_get_request(match): 'Notify on DCC get request.' if weechat.config_get_plugin("show_dcc") == "on": nick = match.group(1) file_name = match.group(2) a_notify( 'DCC', 'File Transfer Request', '{0} wants to send you {1}.'.format(nick, file_name)) def notify_dcc_get_completed(match): 'Notify on DCC get completion.' if weechat.config_get_plugin("show_dcc") == "on": file_name = match.group(1) a_notify('DCC', 'Download Complete', file_name) def notify_dcc_get_failed(match): 'Notify on DCC get failure.' if weechat.config_get_plugin("show_dcc") == "on": file_name = match.group(1) a_notify('DCC', 'Download Failed', file_name) def notify_dcc_send_completed(match): 'Notify on DCC send completion.' if weechat.config_get_plugin("show_dcc") == "on": file_name = match.group(1) a_notify('DCC', 'Upload Complete', file_name) def notify_dcc_send_failed(match): 'Notify on DCC send failure.' if weechat.config_get_plugin("show_dcc") == "on": file_name = match.group(1) a_notify('DCC', 'Upload Failed', file_name) # ----------------------------------------------------------------------------- # Utility # ----------------------------------------------------------------------------- def set_away_status(match): status = match.group(1) if status == 'been ': STATE['is_away'] = True if status == 'longer ': STATE['is_away'] = False def cb_process_message( data, wbuffer, date, tags, displayed, highlight, prefix, message ): '''Delegates incoming messages to appropriate handlers.''' tags = set(tags.split(',')) functions = globals() is_public_message = tags.issuperset( TAGGED_MESSAGES['public message or action']) buffer_name = weechat.buffer_get_string(wbuffer, 'name') dcc_buffer_regex = re.compile(r'^irc_dcc\.', re.UNICODE) dcc_buffer_match = dcc_buffer_regex.match(buffer_name) highlighted = False if int(highlight): highlighted = True # Private DCC message identifies itself as public. if is_public_message and dcc_buffer_match: notify_private_message_or_action(prefix, message, highlighted) return weechat.WEECHAT_RC_OK # Pass identified, untagged message to its designated function. for key, value in UNTAGGED_MESSAGES.items(): match = value.match(message) if match: functions[DISPATCH_TABLE[key]](match) return weechat.WEECHAT_RC_OK # Pass identified, tagged message to its designated function. for key, value in TAGGED_MESSAGES.items(): if tags.issuperset(value): functions[DISPATCH_TABLE[key]](prefix, message, highlighted) return weechat.WEECHAT_RC_OK return weechat.WEECHAT_RC_OK def a_notify(notification, title, description, priority=notify2.URGENCY_LOW): '''Returns whether notifications should be sticky.''' is_away = STATE['is_away'] icon = STATE['icon'] time_out = 5000 if weechat.config_get_plugin('sticky') == 'on': time_out = 0 if weechat.config_get_plugin('sticky_away') == 'on' and is_away: time_out = 0 try: notify2.init("wee-notifier") wn = notify2.Notification(title, description, icon) wn.set_urgency(priority) wn.set_timeout(time_out) wn.show() except Exception as error: weechat.prnt('', 'anotify: {0}'.format(error)) # ----------------------------------------------------------------------------- # Main # ----------------------------------------------------------------------------- def main(): '''Sets up WeeChat notifications.''' # Initialize options. for option, value in SETTINGS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value) # Initialize. name = "WeeChat" icon = "/usr/share/pixmaps/weechat.xpm" notifications = [ 'Public', 'Private', 'Action', 'Notice', 'Invite', 'Highlight', 'Server', 'Channel', 'DCC', 'WeeChat' ] STATE['icon'] = icon # Register hooks. weechat.hook_signal( 'irc_server_connected', 'cb_irc_server_connected', '') weechat.hook_signal( 'irc_server_disconnected', 'cb_irc_server_disconnected', '') weechat.hook_signal('upgrade_ended', 'cb_upgrade_ended', '') weechat.hook_print('', '', '', 1, 'cb_process_message', '') if __name__ == '__main__' and IMPORT_OK and weechat.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '' ): main() weechat-scripts/python/correction_completion.py0000644000175100017510000002407115112622401021140 0ustar manumanu# -*- coding: utf-8 -*- ###################################################################### # Copyright (c) 2011 by Pascal Wittmann # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Marked Parts are from Wojciech Muła # and are licensed under BSD and are avaliable at # http://0x80.pl/proj/aspell-python/ ######################################################################## # INSTALLTION # After copying this file into your python plugin directory, start weechat # load the script and follow futher instructions calling # /help correction_completion # You can find these instructions as markdown on # https://github.com/pSub/weechat-correction-completion/blob/master/README.md # too. # Changelog: # 2020-01-10 -- Version 0.4.0 # - Transition to Python 3 # # 2012-10-18 -- Version 0.3.0 # - incorrect words have a higher priority (typo completion) # - the following characters are removed from the typo: , . ; : ? ! ) ( \ / " ^ # # 2011-06-01 -- Version 0.2.2 # - fixed a memory leak, thanks to FlashCode # - documentation updates # # 2011-03-19 -- Version 0.2.1 # - fixed bug that resulted in a crash of weechat # # 2011-03-18 -- Version 0.2.0 # - fixed out of bounds bug # - apply config changes without reloading # - improved performance # # 2011-02-19 -- Version 0.1.0 # - frist working version import re try: import ctypes import ctypes.util except ImportError: print("This script depends on ctypes") try: import weechat as w except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") SCRIPT_NAME = "correction_completion" SCRIPT_AUTHOR = "Pascal Wittmann " SCRIPT_VERSION = "0.4.0" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Provides a completion for 's/typo/correct'" SCRIPT_COMMAND = "correction_completion" # Default Options # Your can use all aspell options listed on # http://aspell.net/man-html/The-Options.html settings = { 'lang' : 'en', } # The Bunch Class is from # http://code.activestate.com/recipes/52308/ class Bunch: def __init__(self, **kwds): self.__dict__.update(kwds) def completion(data, completion_item, buffer, completion): if state.used == True: return w.WEECHAT_RC_OK else: state.used = True # Current cursor position pos = w.buffer_get_integer(buffer, 'input_pos') # Current input string input = w.buffer_get_string(buffer, 'input') fst = input.find("s/") snd = input.find("/", fst + 2) # Check for typo or suggestion completion if pos > 2 and fst >= 0 and fst < pos: if snd >= 0 and snd < pos: complete_replacement(pos, input, buffer) else: complete_typo(pos, input, buffer) state.used = False return w.WEECHAT_RC_OK def complete_typo(pos, input, buffer): # Assume that typo changes when doing a completion state.curRepl = -1 # Get the text of the current buffer list = [] infolist = w.infolist_get('buffer_lines', buffer, '') while w.infolist_next(infolist): list.append(strip_symbols(w.infolist_string(infolist, 'message'))) w.infolist_free(infolist) # Generate a list of words text = (' '.join(list)).split(' ') # Remove duplicate elements text = unify(text) # Split words in correct and incorrect ones good = [word for word in text if spellcheck(word) == True] bad = [word for word in text if spellcheck(word) == False] # Sort by alphabet and length good.sort(key=lambda item: (item, len(item))) bad.sort(key=lambda item: (item, len(item))) # Place incorrcet ones in front of correct ones text = bad + good i = iter(text) # Get index of last occurence of "s/" befor cursor position n = input.rfind("s/", 0, pos) # Get substring and search the replacement substr = input[n+2:pos] replace = search((lambda word : word.startswith(substr)), i) # If no replacement found, display substring if replace == "": replace = substr # If substring perfectly matched take next replacement if replace == substr: try: replace = next(i) except StopIteration: replace = substr changeInput(substr, replace, input, pos, buffer) def complete_replacement(pos, input, buffer): # Start Positions n = input.rfind("s/", 0, pos) m = input.rfind("/", n + 2, pos) repl = input[m + 1 : pos] typo = input[n + 2 : m] # Only query new suggestions, when typo changed if state.curRepl == -1 or typo != state.curTypo: state.suggestions = suggest(typo) state.curTypo = typo if len(state.suggestions) == 0: return # Start at begining when reached end of suggestions if state.curRepl == len(state.suggestions) - 1: state.curRepl = -1 # Take next suggestion state.curRepl += 1 # Put suggestion into the input changeInput(repl, state.suggestions[state.curRepl], input, pos, buffer) def changeInput(search, replace, input, pos, buffer): # Put the replacement into the input n = len(search) input = '%s%s%s' %(input[:pos-n], replace, input[pos:]) w.buffer_set(buffer, 'input', input) w.buffer_set(buffer, 'input_pos', str(pos - n + len(replace))) def strip_symbols(string): return re_remove_chars.sub('', w.string_remove_color(string, '')) def search(p, i): # Search for item matching the predicate p while True: try: item = next(i) if p(item): return item except StopIteration: return "" def unify(list): # Remove duplicate elements from a list checked = [] for e in list: if e not in checked: checked.append(e) return checked # Parts are from Wojciech Muła def suggest(word): if type(word) is str: suggestions = aspell.aspell_speller_suggest( speller, word.encode(), len(word)) elements = aspell.aspell_word_list_elements(suggestions) list = [] while True: wordptr = aspell.aspell_string_enumeration_next(elements) if not wordptr: break; else: word = ctypes.c_char_p(wordptr) list.append(word.value.decode('UTF-8')) aspell.delete_aspell_string_enumeration(elements) return list else: raise TypeError("String expected") def spellcheck(word): if type(word) is str: return aspell.aspell_speller_check( speller, word, len(word)) else: raise TypeError("String expected") def load_config(data = "", option = "", value = ""): global speller config = aspell.new_aspell_config() for option, default in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default) value = w.config_get_plugin(option) if not aspell.aspell_config_replace( config, option.encode(), value.encode()): raise Exception("Failed to replace config entry") # Error checking is from Wojciech Muła possible_error = aspell.new_aspell_speller(config) aspell.delete_aspell_config(config) if aspell.aspell_error_number(possible_error) != 0: aspell.delete_aspell_can_have_error(possible_error) raise Exception("Couldn't create speller") speller = aspell.to_aspell_speller(possible_error) return w.WEECHAT_RC_OK if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): # Saving the current completion state state = Bunch(used = False, curTypo = '', curRepl = -1, suggestions = []) # Use ctypes to access the apsell library aspell = ctypes.CDLL(ctypes.util.find_library('aspell')) speller = 0 # Regex to remove unwanted characters re_remove_chars = re.compile('[,.;:?!\)\(\\\/\"\^]') # Load configuration load_config() template = 'correction_completion' # Register completion hook w.hook_completion(template, "Completes after 's/' with words from buffer", 'completion', '') # Register hook to update config when option is changed with /set w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "load_config", "") # Register help command w.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, "", """Usage: If you want to correct yourself, you often do this using the expression 's/typo/correct'. This plugin allows you to complete the first part (the typo) by pressing *Tab*. The words from the actual buffer are used to complet this part. If the word can be perfectly matched the next word in alphabetical order is shown. The second part (the correction) can also be completed. Just press *Tab* after the slash and the best correction for the typo is fetched from aspell. If you press *Tab* again, it shows the next suggestion. The language used for suggestions can be set with the option plugins.var.python.correction_completion.lang The aspell language pack must be installed for this language. Setup: Add the template %%(%(completion)s) to the default completion template. The best way to set the template is to use the iset-plugin¹, because you can see there the current value before changing it. Of course you can also use the standard /set-command e.g. /set weechat.completion.default_template "%%(nicks)|%%(irc_channels)|%%(%(completion)s)" Footnotes: ¹ http://weechat.org/scripts/source/stable/iset.pl/ """ %dict(completion=template), '', '', '') weechat-scripts/python/prism.py0000644000175100017510000001406715112622400015675 0ustar manumanu# Copyright (c) 2010 Alex Barrett # # Everyone is permitted to copy and distribute verbatim or modified # copies of this license document, and changing it is allowed as long # as the name is changed. # # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION # # 0. You just DO WHAT THE FUCK YOU WANT TO. # 2019-08-24, simonpatapon # v0.2.12: make the script compatible with python 3 # 2017-06-28, Aoede # v0.2.11: add -k switch to add black background # 2015-11-16, wowaname # v0.2.9, 0.2.10: wrote an actual parser rather than regex # 2014-09-03, Matthew Martin # v0.2.8: add color reset to the end of the output # 2013-11-26, Seganku # v0.2.7: add -c switch for the option to pass output to a command # 2013-07-19, Sebastien Helleu # v0.2.6: use buffer received in command callback instead of current buffer # 2013-05-04, Rylai # v0.2.5: add -e switch for the option to destroy the eyes of all # who have the misfortune of seeing your text # 2013-04-26, Biohazard # v0.2.4: add support for using the command through keybindings # 2013-03-12, R1cochet # v0.2.3: add -b switch for backwards/reverse text # 2013-01-29, SuperT1R: # v0.2.2: add -m switch to append /me to the beginning of the output import weechat as w import random import re import sys SCRIPT_NAME = "prism" SCRIPT_AUTHOR = "Alex Barrett " SCRIPT_VERSION = "0.2.12" SCRIPT_LICENSE = "WTFPL" SCRIPT_DESC = "Taste the rainbow." # red, lightred, brown, yellow, green, lightgreen, cyan, # lightcyan, blue, lightblue, magenta, lightmagenta colors = [5, 4, 7, 8, 3, 9, 10, 11, 2, 12, 6, 13] color_count = len(colors) # keeping a global index means the coloring will pick up where it left off color_index = 0 # spaces don't need to be colored and commas cannot be because mIRC is dumb chars_neutral = " ," chars_control = "\x01-\x1f\x7f-\x9f" regex_chars = "[^%(n)s%(s)s][%(n)s%(s)s]*" % { 'n': chars_neutral, 's': chars_control } regex_words = "[^%(n)s]+[%(n)s%(s)s]*" % { 'n': chars_neutral, 's': chars_control } if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): w.hook_command("prism", SCRIPT_DESC, "[-rwmbek] text|-c[wbe] text", " -r: randomizes the order of the color sequence\n" " -w: color entire words instead of individual characters\n" " -m: append /me to beginning of output\n" " -b: backwards text (entire string is reversed)\n" " -e: eye-destroying colors (randomized background colors)\n" " -k: add black background (note: -e overrides this)\n" " -c: specify a separator to turn on colorization\n" " eg. -c : /topic :howdy howdy howdy\n" " text: text to be colored", "-r|-w|-m|-b|-e|-c", "prism_cmd_cb", "") def find_another_color(colorCode): otherColor = (str(colors[random.randint(1, color_count - 1) % color_count]).rjust(2, "0")) try: otherColor = otherColor.decode('utf-8') except AttributeError: pass while (otherColor == colorCode): otherColor = (str(colors[random.randint(1, color_count - 1) % color_count]).rjust(2, "0")) try: otherColor = otherColor.decode('utf-8') except AttributeError: pass return otherColor def prism_cmd_cb(data, buffer, args): global color_index try: input = args.decode('utf-8') except AttributeError: input = args input_method = "command" if not input or (input[0] == '-' and input.find(' ') == -1): input = (input + ' ' if input else '') + w.buffer_get_string(buffer, "input") try: input = input.decode('utf-8') except AttributeError: pass input_method = "keybinding" if not input: return w.WEECHAT_RC_OK optstop = input and input[0] == '-' and input.find(' ') opts = input[1:optstop] if optstop else '' cmdstop = 'c' in opts and input.find(' ', optstop+1) cmd = '' if 'm' in opts: cmd = '/me ' if 'c' in opts: find = input[optstop+1:cmdstop] where = input.find(find, cmdstop+1) cmd = input[cmdstop+1:where] input = input[where+len(find):] else: input = input[optstop+bool(optstop):] regex = regex_words if 'w' in opts else regex_chars inc = 'r' not in opts bs = 'e' in opts k = 'k' in opts input = input[::-1] if 'b' in opts else input output = "" tokens = re.findall(regex, input) for token in tokens: # prefix each token with a color code color_code = str(colors[color_index % color_count]).rjust(2, "0") try: color_code = color_code.decode('utf-8') except AttributeError: pass if bs == 1: output += '\x03' + color_code + ',' + find_another_color(color_code) + token elif k == 1: output += '\x03' + color_code + ',' + '1'.rjust(2, "0") + token else: output += "\x03" + color_code + token # select the next color or another color at # random depending on the options specified if inc == 0: color_index += random.randint(1, color_count - 1) else: color_index += inc output += '\x0f' # output starting with a / will be executed as a # command unless we escape it with a preceding / # Commands should use the -c flag if len(output) > 0 and output[0] == "/": output = "/" + output if len(cmd) > 0: output = cmd + output if sys.version_info < (3, ): output = output.encode('utf-8') if input_method == "keybinding": w.buffer_set(buffer, "input", output) else: w.command(buffer, output) return w.WEECHAT_RC_OK weechat-scripts/python/buffer_autoclose.py0000644000175100017510000001277115112622377020107 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2009 by xt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # (this script requires WeeChat 0.3.0 or newer) # # History: # 2024-05-04, Miklos Vajna # version 0.6: Allow autoclosing explicitly listed non-private buffers as well # 2018-04-10, Sébastien Helleu # version 0.5: fix infolist_time for WeeChat >= 2.2 (WeeChat returns a long # integer instead of a string) # 2016-02-05, ixti # version 0.4: Add Python3 support # 2009-12-15, xt # version 0.3: moved around some control structures to not be as noisy # 2009-12-02, xt # version 0.2: bugfix, more printing # 2009-12-01, xt # version 0.1: initial release import weechat as w import time SCRIPT_NAME = "buffer_autoclose" SCRIPT_AUTHOR = "xt " SCRIPT_VERSION = "0.6" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Automatically close inactive private message buffers" settings = { 'interval': '1', # How often in minutes to check 'age_limit': '30', # How old in minutes before auto close 'ignore': '', # Buffers to ignore (use full name: server.buffer_name) 'prefer': '', # Buffers to prefer, even if they are not private (use full name: server.buffer_name) } if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) w.hook_timer(\ int(w.config_get_plugin('interval')) * 1000 * 60, 0, 0, "close_time_cb", '') def get_all_buffers(): '''Returns list with pointers of all open buffers.''' buffers = [] infolist = w.infolist_get('buffer', '', '') preferlist = w.config_get_plugin('prefer').split(',') while w.infolist_next(infolist): buffer_type = w.buffer_get_string(w.infolist_pointer(infolist, 'pointer'), 'localvar_type') name = w.buffer_get_string(w.infolist_pointer(infolist, 'pointer'), 'name') if name in preferlist: buffers.append(w.infolist_pointer(infolist, 'pointer')) continue if buffer_type == 'private': # we only close private message buffers for now buffers.append(w.infolist_pointer(infolist, 'pointer')) w.infolist_free(infolist) return buffers def get_last_line_date(buffer): date = '1970-01-01 01:00:00' infolist = w.infolist_get('buffer_lines', buffer, '') while w.infolist_prev(infolist): date = w.infolist_time(infolist, 'date') # since WeeChat 2.2, infolist_time returns a long integer instead of # a string if not isinstance(date, str): date = time.strftime('%F %T', time.localtime(int(date))) if date != '1970-01-01 01:00:00': # Some lines like "Day changed to" message doesn't have date # set so loop until we find a message that does break w.infolist_free(infolist) return date def is_in_hotlist(buffer): ''' Returns true if buffer is in hotlist, false if not''' hotlist = w.infolist_get('hotlist', '', '') found = False while w.infolist_next(hotlist): thebuffer = w.infolist_pointer(hotlist, 'buffer_pointer') if thebuffer == buffer: found = True name = w.buffer_get_string(thebuffer, 'short_name') break w.infolist_free(hotlist) return found def close_time_cb(buffer, args): ''' Callback for check for inactivity and close ''' for buffer in get_all_buffers(): name = w.buffer_get_string(buffer, 'name') date = get_last_line_date(buffer) date = time.mktime(time.strptime(date, '%Y-%m-%d %H:%M:%S')) now = time.time() seconds_old = now - date if seconds_old > int(w.config_get_plugin('age_limit'))*60: if is_in_hotlist(buffer): #w.prnt('', '%s: Not closing buffer: %s: it is in hotlist' %(SCRIPT_NAME, name)) continue if name in w.config_get_plugin('ignore').split(','): #w.prnt('', '%s: Not closing buffer: %s: it is in ignore list' %(SCRIPT_NAME, name)) continue if buffer == w.current_buffer(): # Never close current buffer #w.prnt('', '%s: Not closing buffer: %s: it is in currently active' %(SCRIPT_NAME, name)) continue if len(w.buffer_get_string(buffer, 'input')): # Don't close buffers with text on input line #w.prnt('', '%s: Not closing buffer: %s: it has input' %(SCRIPT_NAME, name)) continue w.prnt('', '%s: Closing buffer: %s' %(SCRIPT_NAME, name)) w.command(buffer, '/buffer close') #else: # w.prnt('', '%s: Not closing buffer: %s: it is too new: %s' %(SCRIPT_NAME, name, seconds_old)) return w.WEECHAT_RC_OK weechat-scripts/python/glitter.py0000644000175100017510000000171315112622407016216 0ustar manumanuimport weechat, re SCRIPT_NAME = "glitter" SCRIPT_AUTHOR = "jotham.read@gmail.com" SCRIPT_VERSION = "0.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Replaces ***text*** you write with rainbow text" if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): weechat.hook_command_run("/input return", "command_run_input", "") glitter_pat = re.compile("\*\*\*([^\*]+)\*\*\*") def glitter_it(match): lut = ("13","4","8","9","11","12") # len=6 text = match.group(1) return "".join(["\03"+lut[i%6]+text[i] for i in range(len(text))]) + "\03" def command_run_input(data, buffer, command): if command == "/input return": input = weechat.buffer_get_string(buffer, 'input') if input.startswith('/set '): # Skip modification of settings return weechat.WEECHAT_RC_OK input = glitter_pat.sub(glitter_it, input) weechat.buffer_set(buffer, 'input', input) return weechat.WEECHAT_RC_OK weechat-scripts/python/infolist.py0000644000175100017510000002103615112622377016401 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2008-2018 Sébastien Helleu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Display infolist in a buffer. # # History: # # 2018-04-10, Sébastien Helleu : # version 0.7: fix infolist_time for WeeChat >= 2.2 (WeeChat returns a long # integer instead of a string), fix PEP8 errors # 2017-10-22, nils_2 : # version 0.6: add string_eval_expression() # 2012-10-02, nils_2 : # version 0.5: switch to infolist buffer (if exists) when command /infolist # is called with arguments, add some examples to help page # 2012-01-03, Sébastien Helleu : # version 0.4: make script compatible with Python 3.x # 2010-01-23, m4v : # version 0.3: user can give a pointer as argument # 2010-01-18, Sébastien Helleu : # version 0.2: use tag "no_filter" for lines displayed, fix display bug # when infolist is empty # 2009-11-30, Sébastien Helleu : # version 0.1: first version # 2008-12-12, Sébastien Helleu : # script creation SCRIPT_NAME = "infolist" SCRIPT_AUTHOR = "Sébastien Helleu " SCRIPT_VERSION = "0.7" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Display infolist in a buffer" import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False try: import time except ImportError as message: print('Missing package(s) for %s: %s' % (SCRIPT_NAME, message)) import_ok = False infolist_buffer = "" infolist_var_type = { "i": "int", "s": "str", "p": "ptr", "t": "tim", "b": "buf", } def infolist_buffer_set_title(buffer): # get list of infolists available list_infolists = "" infolist = weechat.infolist_get("hook", "", "infolist") while weechat.infolist_next(infolist): list_infolists += " %s" % weechat.infolist_string(infolist, "infolist_name") weechat.infolist_free(infolist) # set buffer title weechat.buffer_set(buffer, "title", "%s %s | Infolists:%s" % ( SCRIPT_NAME, SCRIPT_VERSION, list_infolists)) def infolist_display(buffer, args): global infolist_var_type items = args.split(" ", 1) infolist_args = "" infolist_pointer = "" if len(items) >= 2: infolist_args = items[1] if infolist_args[:2] == "0x": infolist_pointer, sep, infolist_args = infolist_args.partition(" ") elif infolist_args[:3] == "\"\" ": infolist_args = infolist_args[3:] infolist = weechat.infolist_get(items[0], infolist_pointer, infolist_args) if infolist == "": weechat.prnt_date_tags(buffer, 0, "no_filter", "%sInfolist '%s' not found." % (weechat.prefix("error"), items[0])) return weechat.WEECHAT_RC_OK item_count = 0 weechat.buffer_clear(buffer) weechat.prnt_date_tags( buffer, 0, "no_filter", "Infolist '%s', with pointer '%s' and arguments '%s':" % ( items[0], infolist_pointer, infolist_args)) weechat.prnt(buffer, "") count = 0 while weechat.infolist_next(infolist): item_count += 1 if item_count > 1: weechat.prnt(buffer, "") fields = weechat.infolist_fields(infolist).split(",") prefix = "%s[%s%d%s]\t" % (weechat.color("chat_delimiters"), weechat.color("chat_buffer"), item_count, weechat.color("chat_delimiters")) for field in fields: (type, name) = field.split(":", 1) value = "" quote = "" if type == "i": value = weechat.infolist_integer(infolist, name) elif type == "s": value = weechat.infolist_string(infolist, name) quote = "'" elif type == "p": value = weechat.infolist_pointer(infolist, name) elif type == "t": value = weechat.infolist_time(infolist, name) # since WeeChat 2.2, infolist_time returns a long integer # instead of a string if not isinstance(value, str): str_date = time.strftime('%F %T', time.localtime(int(value))) value = '%d (%s)' % (value, str_date) name_end = "." * (30 - len(name)) weechat.prnt_date_tags( buffer, 0, "no_filter", "%s%s%s: %s%s%s %s%s%s%s%s%s" % (prefix, name, name_end, weechat.color("brown"), infolist_var_type[type], weechat.color("chat"), weechat.color("chat"), quote, weechat.color("cyan"), value, weechat.color("chat"), quote)) prefix = "" count += 1 if count == 0: weechat.prnt_date_tags(buffer, 0, "no_filter", "Empty infolist.") weechat.infolist_free(infolist) return weechat.WEECHAT_RC_OK def infolist_buffer_input_cb(data, buffer, input_data): if input_data == "q" or input_data == "Q": weechat.buffer_close(buffer) else: infolist_display(buffer, input_data) return weechat.WEECHAT_RC_OK def infolist_buffer_close_cb(data, buffer): global infolist_buffer infolist_buffer = "" return weechat.WEECHAT_RC_OK def infolist_buffer_new(): global infolist_buffer infolist_buffer = weechat.buffer_search("python", "infolist") if infolist_buffer == "": infolist_buffer = weechat.buffer_new("infolist", "infolist_buffer_input_cb", "", "infolist_buffer_close_cb", "") if infolist_buffer != "": infolist_buffer_set_title(infolist_buffer) weechat.buffer_set(infolist_buffer, "localvar_set_no_log", "1") weechat.buffer_set(infolist_buffer, "time_for_each_line", "0") weechat.buffer_set(infolist_buffer, "display", "1") def infolist_cmd(data, buffer, args): global infolist_buffer args = string_eval_expression(args) if infolist_buffer == "": infolist_buffer_new() if infolist_buffer != "" and args != "": infolist_display(infolist_buffer, args) weechat.buffer_set(infolist_buffer, "display", "1") return weechat.WEECHAT_RC_OK def string_eval_expression(string): return weechat.string_eval_expression(string, {}, {}, {}) if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): weechat.hook_command( "infolist", "Display infolist in a buffer", "[infolist [pointer] [arguments]]", " infolist: name of infolist\n" " pointer: optional pointer for infolist (\"\" for none)\n" "arguments: optional arguments for infolist\n\n" "Command without argument will open buffer used " "to display infolists.\n\n" "On infolist buffer, you can enter name of an " "infolist, with optional arguments.\n" "Enter 'q' to close infolist buffer.\n\n" "Examples:\n" " Show information about nick \"FlashCode\" in channel " "\"#weechat\" on server \"freenode\":\n" " /infolist irc_nick freenode,#weechat,FlashCode\n" " Show nicklist from a specific buffer:\n" " /infolist nicklist \n" " Show current buffer:\n" " /infolist buffer ${buffer}" "", "%(infolists)", "infolist_cmd", "") weechat-scripts/python/alternatetz.py0000644000175100017510000000372615112622377017115 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2010 Chmouel Boudjnah # Copyright (C) 2012-2013 bwidawsk # License: GPL3 # # plugin to get alternate timezones in a weechat bar # # Changelog: # 0.4 Sébastien Helleu # Remove trailing tabs # 0.3 Pol Van Aubel # Fix tab/space usage for Python3 compatibility # 0.2 Added help, and multiple timezeones # 0.1 first version # import weechat as w import pytz import datetime SCRIPT_NAME = "alternatetz" SCRIPT_AUTHOR = "Chmouel Boudjnah " SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Display Alternate Time from different Timezones" SCRIPT_COMMAND = 'alternatetz' OPTIONS = { 'timezone': ('GMT', 'list of timezones to display. The list is comprised of space separated list timezones using the Olson tz database'), 'timeformat': ('%H:%M', 'strftime compatible format') } def alternatetz_item_cb(*kwargs): ret = '' tznames = OPTIONS['timezone'].split() for tzname in tznames: tz = pytz.timezone(tzname) ret += tz.zone + ': ' + datetime.datetime.now(tz).strftime(OPTIONS['timeformat']) + ' ' return ret[:-1] def alternatetz_timer_cb(*kwargs): w.bar_item_update('alternatetz') return w.WEECHAT_RC_OK if __name__ == '__main__': w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '') for option,value in list(OPTIONS.items()): w.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) if not w.config_is_set_plugin(option): w.config_set_plugin(option, value[0]) OPTIONS[option] = value[0] else: OPTIONS[option] = w.config_get_plugin(option) w.bar_item_new('alternatetz', 'alternatetz_item_cb', '') w.bar_item_update('alternatetz') w.hook_timer(1000*60, 60, 0, 'alternatetz_timer_cb', '') # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 weechat-scripts/python/notify.py0000644000175100017510000001426115112622376016063 0ustar manumanu# -*- coding: utf-8 -*- # Author: lavaramano # Improved by: BaSh - # Ported to Weechat 0.3.0 by: Sharn - # version 0.0.9: make script compatible with Python 3 # 2014-05-10, Sébastien Helleu # version 0.0.8: change hook_print callback argument type of # displayed/highlight (WeeChat >= 1.0) # 2013-11-17, spline # version 0.0.7: Fix up prnt statement in previous edition. # add in option to not notify on messages starting with prefix like * # this is to stop getting notifications if user is connected via ZNC # where you command ZNC via irc and all its targets start with '*'. # 2013-11-16, spline # version 0.0.6: Convert from pynotify to notify2 (pynotify is being retired) # clean up .show() so it's in a try/except block so we don't get errors in console # we also add in an option to disable notifications when away. # improve privmsg highlight so if we /msg someone but not in current buffer, it won't notify. # 2010-02-20, Aron Griffis # version 0.0.5: Add nick_separator, don't call show_notification twice on # privmsg, fix spelling s/nofify/notify/, use nick as "summary" for privmsg # notification, fit in 80 columns, tweak vim modeline. # 2010-01-24, David Rubin # version 0.0.4.2 Fixed issue with self notifications when used with out "smart_notification" # 2010-01-19, Didier Roche # version 0.0.4.1: add private message sender name # 2010-01-19, Didier Roche # version 0.0.4: add smart notification: # be notified only if you're not in the current channel/pv window (off by default) # 2009-06-16, kba : # version 0.0.3: added config options for icon and urgency # 2009-05-02, FlashCode : # version 0.0.2.1: sync with last API changes from __future__ import print_function # script variables SCRIPT_NAME = "notify" SCRIPT_AUTHOR = "lavaramano" SCRIPT_VERSION = "0.0.9" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "notify: A real time notification system for weechat" # make sure we're run under weechat. import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False # make sure we have notify2. try: import notify2 except ImportError as message: print("Missing package(s) for %s: %s" % (SCRIPT_NAME, message)) print("You must have notify2 installed.") import_ok = False # script options settings = { "show_hilights" : "on", "show_priv_msg" : "on", "notify_when_away" : "off", "nick_separator" : ": ", "ignore_nicks_startwith" : "*", "icon" : "/usr/share/pixmaps/weechat.xpm", "urgency" : "normal", "smart_notification" : "off" } urgencies = { "low" : notify2.URGENCY_LOW, "critical" : notify2.URGENCY_CRITICAL, "normal" : notify2.URGENCY_NORMAL, } # Functions def notify_show(data, bufferp, uber_empty, tagsn, isdisplayed, ishilight, prefix, message): """Sends highlighted message to be printed on notification""" # string for show_notification return snreturn = None # smart_notification if (weechat.config_get_plugin('smart_notification') == "on" and bufferp == weechat.current_buffer()): pass # are we away and want highlights? check: w.infolist_integer(infolist, 'is_away') elif (weechat.config_get_plugin('notify_when_away') == "off" and weechat.buffer_get_string(bufferp, 'localvar_away')): pass elif (weechat.buffer_get_string(bufferp, "localvar_type") == "private" and weechat.config_get_plugin('show_priv_msg') == "on"): # should we ignore messages from something like ZNC with * prefix? ignprefix = weechat.config_get_plugin('ignore_nicks_startwith') if ignprefix != '' and prefix.startswith(ignprefix): # if not empty.. pass # if im sending a message to someone, don't pop up a notification. elif weechat.buffer_get_string(bufferp, "localvar_nick") != prefix: snreturn = show_notification(prefix, message) elif (int(ishilight) and weechat.config_get_plugin('show_hilights') == "on"): buffer = (weechat.buffer_get_string(bufferp, "short_name") or weechat.buffer_get_string(bufferp, "name")) snreturn = show_notification(buffer, prefix + weechat.config_get_plugin('nick_separator') + message) # check to see if we had an error showing notification and return to user if snreturn: weechat.prnt(bufferp, snreturn) return weechat.WEECHAT_RC_OK def show_notification(chan, message): """Our handler to print highlighted messages""" notify2.init("weechat") wn = notify2.Notification(chan, message, weechat.config_get_plugin('icon')) wn.set_urgency(urgencies[weechat.config_get_plugin('urgency')] or notify2.URGENCY_NORMAL) # now try to show notification try: wn.show() return None except Exception as e: return "Exception trying to show notification: {0}".format(e) if __name__ == "__main__": if import_ok and weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): # Init everything for option, default_value in settings.items(): if weechat.config_get_plugin(option) == "": weechat.config_set_plugin(option, default_value) # Hook privmsg/hilights weechat.hook_print("", "irc_privmsg", "", 1, "notify_show", "") # w.hook_info('%s_buffer' %SCRIPT_NAME, '', '', 'info_hook_cb', '') # vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=250: weechat-scripts/python/detach_away.py0000644000175100017510000000442715112622406017021 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2017 p3lim # # https://github.com/p3lim/weechat-detach-away # # Changelog: # Ver: 0.1.1 Python3 support by Antonin Skala skala.antonin@gmail.com 3.2019 # Ver: 0.1.2 Support Python 2 and 3 by Antonin Skala skala.antonin@gmail.com 3.2019 try: import weechat except ImportError: from sys import exit print('This script has to run under WeeChat (https://weechat.org/).') exit(1) import sys if sys.version_info[0] > 2: from urllib.parse import urlencode else: from urllib import urlencode SCRIPT_NAME = 'detach_away' SCRIPT_AUTHOR = 'p3lim' SCRIPT_VERSION = '0.1.2' SCRIPT_LICENSE = 'MIT' SCRIPT_DESC = 'Automatically sets away message based on number of relays connected' SETTINGS = { 'message': ( 'I am away', 'away message'), 'debugging': ( 'off', 'debug flag'), } num_relays = 0 def DEBUG(): return weechat.config_get_plugin('debug') == 'on' def set_away(is_away, message=''): if is_away: message = weechat.config_get_plugin('message') weechat.command('', '/away -all ' + message) def relay_connected(data, signal, signal_data): global num_relays if DEBUG(): weechat.prnt('', 'DETACH_AWAY: last #relays: ' + str(num_relays)) if int(num_relays) == 0: set_away(False) num_relays = weechat.info_get('relay_client_count', 'connected') return weechat.WEECHAT_RC_OK def relay_disconnected(data, signal, signal_data): global num_relays if DEBUG(): weechat.prnt('', 'DETACH_AWAY: last #relays: ' + str(num_relays)) if int(num_relays) > 0: set_away(True) num_relays = weechat.info_get('relay_client_count', 'connected') return weechat.WEECHAT_RC_OK # register plugin weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '') # register for relay status weechat.hook_signal('relay_client_connected', 'relay_connected', '') weechat.hook_signal('relay_client_disconnected', 'relay_disconnected', '') # register configuration defaults for option, value in SETTINGS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) weechat-scripts/python/fileaway.py0000644000175100017510000001716415112622401016346 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2011 by Richard A Hofer (javagamer) # # fileaway.py - A simple autoaway script for Weechat which monitors a file, # allowing it to easily connect to external things (such as xscreensaver) # # The code from screen_away.py and auto_away.py were heavily consulted in the # writing of this script # --------------------------------------------------------------------------- # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # ------------------------------------------------------------------------- # Purpose - # I wrote this auto-away script so that it could easily be hooked into other # things to deterine whether or not I was there rather than just watching for # keyboard input in Weechat. In my case I wanted to be able to have weechat in # a tmux session on a server, yet still go away when I lock the screen on my # desktop. # # Below is a sample shellscript which watches xscreensaver and sets away when # it is locked, and available when unlocked # While this only one way this script can be used, this is why I wrote it # # #!/bin/sh # # # Read xscreensaver's state # xscreensaver-command -watch| # while read STATUS; do # case "$STATUS" in # LOCK*) # rm ~/.available # ;; # UNBLANK*) # touch ~/.available # ;; # esac # done # # Alternative for xset: # # #!/bin/bash # # xset -display ":0" q|grep "Monitor"|awk ' { print $3 $4 } '| # while read MONITOR; do # case "$MONITOR" in # Off) # rm ~/.available # ;; # On) # touch ~/.available # ;; # esac # done # ------------------------------------------------------------------------ # Changelog: # Version 1.0 released - March 27, 2011 # -Initial release # Version 1.0.1 released - March 31, 2011 # -Handles improper commands # Version 1.0.2 release - Jun 15, 2011 # -Added alternative for xset users (credit: sherpa9 at irc.freenode.net) # Version 1.0.3 release - Dec 19, 2019 # -Adapt for python3 from __future__ import print_function try: import weechat as w import os.path except Exception: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.wwchat.org/") quit() SCRIPT_NAME = "fileaway" SCRIPT_AUTHOR = "javagamer" SCRIPT_VERSION = "1.0.3" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Set away status based on presence of a file" debug = 0 TIMER = None settings = { 'filepath': './.available', 'awaymessage':'Away', 'interval': '20', # How often to check for inactivity (in seconds) 'status': '0', 'away': '0', } def set_back(overridable_messages): '''Removes away status for servers where one of the overridable_messages is set''' if(w.config_get_plugin('away') == '0'): return # No need to come back again serverlist = w.infolist_get('irc_server','','') if serverlist: buffers = [] while w.infolist_next(serverlist): if w.infolist_string(serverlist, 'away_message') in overridable_messages: ptr = w.infolist_pointer(serverlist, 'buffer') if ptr: buffers.append(ptr) w.infolist_free(serverlist) for buffer in buffers: w.command(buffer, "/away") w.config_set_plugin('away', '0') def set_away(message, overridable_messages=[]): '''Sets away status, but respectfully (so it doesn't change already set statuses''' if(w.config_get_plugin('away') == '1'): return # No need to go away again (this prevents some repeated messages) if(debug): w.prnt('', "Setting away to %s" % message) serverlist = w.infolist_get('irc_server','','') if serverlist: buffers = [] while w.infolist_next(serverlist): if w.infolist_integer(serverlist, 'is_away') == 0: if(debug): w.prnt('', "Not away on %s" % w.infolist_string(serverlist, 'name')) ptr = w.infolist_pointer(serverlist, 'buffer') if ptr: buffers.append(ptr) elif w.infolist_string(serverlist, 'away_message') in overridable_messages: if(debug): w.prnt('', "%s is in %s" % (w.infolist_string(serverlist, 'away_message'), repr(overridable_messages))) buffers.append(w.infolist_pointer(serverlist, 'buffer')) w.infolist_free(serverlist) if(debug): w.prnt('', repr(buffers)) for buffer in buffers: w.command(buffer, "/away %s" % message) w.config_set_plugin('away', '1') def fileaway_cb(data, buffer, args): response = { 'enable' : lambda args: w.config_set_plugin('status', '1') and check_timer(), 'disable' : lambda args: w.config_set_plugin('status', '0') and check_timer(), 'check' : check, 'file' : lambda filepath: w.config_set_plugin('filepath', filepath), 'interval': lambda interval: w.config_set_plugin('interval', interval), 'msg' : lambda status: w.config_set_plugin('awaymessage', status), } if args: words = args.strip().partition(' ') if words[0] in response: response[words[0]](words[2]) else: w.prnt('', "Fileaway error: %s not a recognized command. Try /help fileaway" % words[0]) w.prnt('', "fileaway: enabled: %s interval: %s away message: \"%s\" filepath: %s" % (w.config_get_plugin('status'), w.config_get_plugin('interval'), w.config_get_plugin('awaymessage'), w.config_get_plugin('filepath'))) return w.WEECHAT_RC_OK def auto_check(data, remaining_calls): '''Callback from timer''' check(0) return w.WEECHAT_RC_OK def check(args): '''Check for existance of file and set away if it isn't there''' if os.path.isfile(w.config_get_plugin('filepath')): set_back([w.config_get_plugin('awaymessage')]) else: set_away(w.config_get_plugin('awaymessage'), []) def check_timer(): '''Sets or unsets the timer based on whether or not the plugin is enabled''' global TIMER if TIMER: w.unhook(TIMER) if w.config_get_plugin('status') == '1': TIMER = w.hook_timer(int(w.config_get_plugin('interval')) * 1000, 0, 0, "auto_check", "") w.prnt('', "fileaway timer is running.") if __name__ == "__main__": if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) w.hook_command("fileaway", "Set away status based on presense or absense of a file.", "check, msg [status], interval [time], file [filepath], or enable|disable", "check - manually checks for file rather than waiting for interval.\n" "msg [status] - sets the away message" "interval [time] - sets the interval to check for the file.\n" "file [filepath] - sets the file to be watched.\n" "enable|disable - enables or disables plugin.\n", "check" " || msg" " || interval" " || file %(filename)" " || enable" " || disable", "fileaway_cb", "") check_timer() if(w.config_get_plugin('status') == '0'): w.prnt('', "fileaway is currently disabled. Type /fileaway enable to enable it.") else: w.prnt('', "fileaway is currently enabled. Type /fileaway disable to disable it.") weechat-scripts/python/jabber.py0000644000175100017510000021552515112622400015772 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2009-2013 Sebastien Helleu # Copyright (C) 2010 xt # Copyright (C) 2010 Aleksey V. Zapparov # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Jabber/XMPP protocol for WeeChat. # (this script requires WeeChat 0.3.0 (or newer) and xmpppy library) # # For help, see /help jabber # Happy chat, enjoy :) # # History: # 2013-09-30, Nils Görs : # version 1.6: add support of /secure for passwords and jid # : fix stdout/stderr when no JID was set # 2013-05-14, Billiam : # version 1.5: fix unicode encoding error in /jabber buddies # 2013-05-03, Sebastien Helleu : # version 1.4: add tags in user messages: notify_xxx, no_highlight, # nick_xxx, prefix_nick_xxx, log1 # 2012-05-12, Sebastian Rydberg : # version 1.3: Added support for fetching names from roster # 2012-04-11, Sebastien Helleu : # version 1.2: fix deletion of server options # 2012-03-09, Sebastien Helleu : # version 1.1: fix reload of config file # 2012-01-03, Sebastien Helleu : # version 1.0: changes for future compatibility with Python 3.x # 2011-12-15, Sebastien Helleu : # version 0.9: fix utf-8 encoding problem on jid # 2011-03-21, Isaac Raway : # version 0.8: search chat buffer before opening it # 2011-02-13, Sebastien Helleu : # version 0.7: use new help format for command arguments # 2010-11-23, xt # version 0.6: change format of sent ping, to match RFC # 2010-10-05, xt, # version 0.5: no highlight for status/presence messages # 2010-10-01, xt, # version 0.4: # add kick and invite # 2010-08-03, Aleksey V. Zapparov : # version 0.3: # add /jabber priority [priority] # add /jabber status [message] # add /jabber presence [online|chat|away|xa|dnd] # 2010-08-02, Aleksey V. Zapparov : # version 0.2.1: # fix prexence is set for current resource instead of sending # special presences for all buddies # 2010-08-02, Aleksey V. Zapparov : # version 0.2: # add priority and away_priority of resource # 2010-08-02, Sebastien Helleu : # version 0.1: first official version # 2010-08-01, ixti : # fix bug with non-ascii resources # 2010-06-09, iiijjjiii : # add connect server and port options (required for google talk) # add private option permitting messages to be displayed in separate # chat buffers or in a single server buffer # add jid aliases # add keepalive ping # 2010-03-17, xt : # add autoreconnect option, autoreconnects on protocol error # 2010-03-17, xt : # add autoconnect option, add new command /jmsg with -server option # 2009-02-22, Sebastien Helleu : # first version (unofficial) # SCRIPT_NAME = "jabber" SCRIPT_AUTHOR = "Sebastien Helleu " SCRIPT_VERSION = "1.6" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Jabber/XMPP protocol for WeeChat" SCRIPT_COMMAND = SCRIPT_NAME import re import warnings import_ok = True try: import weechat except: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False # On import, xmpp may produce warnings about using hashlib instead of # deprecated sha and md5. Since the code producing those warnings is # outside this script, catch them and ignore. original_filters = warnings.filters[:] warnings.filterwarnings("ignore",category=DeprecationWarning) try: import xmpp except: print("Package python-xmpp (xmpppy) must be installed to use Jabber protocol.") print("Get xmpppy with your package manager, or at this URL: http://xmpppy.sourceforge.net/") import_ok = False finally: warnings.filters = original_filters # ==============================[ global vars ]=============================== jabber_servers = [] jabber_server_options = { "jid" : { "type" : "string", "desc" : "jabber id (user@server.tld)", "min" : 0, "max" : 0, "string_values": "", "default" : "", "value" : "", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "priority" : { "type" : "integer", "desc" : "Default resource priority", "min" : 0, "max" : 65535, "string_values": "", "default" : "8", "value" : "8", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "away_priority": { "type" : "integer", "desc" : "Resource priority on away", "min" : 0, "max" : 65535, "string_values": "", "default" : "0", "value" : "0", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "password" : { "type" : "string", "desc" : "password for jabber id on server", "min" : 0, "max" : 0, "string_values": "", "default" : "", "value" : "", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "server" : { "type" : "string", "desc" : "connect server host or ip, eg. talk.google.com", "min" : 0, "max" : 0, "string_values": "", "default" : "", "value" : "", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "port" : { "type" : "integer", "desc" : "connect server port, eg. 5223", "min" : 0, "max" : 65535, "string_values": "", "default" : "5222", "value" : "5222", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "autoconnect" : { "type" : "boolean", "desc" : "automatically connect to server when script is starting", "min" : 0, "max" : 0, "string_values": "", "default" : "off", "value" : "off", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "autoreconnect": { "type" : "boolean", "desc" : "automatically reconnect to server when disconnected", "min" : 0, "max" : 0, "string_values": "", "default" : "off", "value" : "off", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "private" : { "type" : "boolean", "desc" : "display messages in separate chat buffers instead of a single server buffer", "min" : 0, "max" : 0, "string_values": "", "default" : "on", "value" : "on", "check_cb" : "", "change_cb" : "", "delete_cb" : "", }, "ping_interval": { "type" : "integer", "desc" : "Number of seconds between server pings. 0 = disable", "min" : 0, "max" : 9999999, "string_values": "", "default" : "0", "value" : "0", "check_cb" : "ping_interval_check_cb", "change_cb" : "", "delete_cb" : "", }, "ping_timeout" : { "type" : "integer", "desc" : "Number of seconds to allow ping to respond before timing out", "min" : 0, "max" : 9999999, "string_values": "", "default" : "10", "value" : "10", "check_cb" : "ping_timeout_check_cb", "change_cb" : "", "delete_cb" : "", }, } jabber_config_file = None jabber_config_section = {} jabber_config_option = {} jabber_jid_aliases = {} # { 'alias1': 'jid1', 'alias2': 'jid2', ... } # =================================[ config ]================================= def jabber_config_init(): """ Initialize config file: create sections and options in memory. """ global jabber_config_file, jabber_config_section jabber_config_file = weechat.config_new("jabber", "jabber_config_reload_cb", "") if not jabber_config_file: return # look jabber_config_section["look"] = weechat.config_new_section( jabber_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", "") if not jabber_config_section["look"]: weechat.config_free(jabber_config_file) return jabber_config_option["debug"] = weechat.config_new_option( jabber_config_file, jabber_config_section["look"], "debug", "boolean", "display debug messages", "", 0, 0, "off", "off", 0, "", "", "", "", "", "") # color jabber_config_section["color"] = weechat.config_new_section( jabber_config_file, "color", 0, 0, "", "", "", "", "", "", "", "", "", "") if not jabber_config_section["color"]: weechat.config_free(jabber_config_file) return jabber_config_option["message_join"] = weechat.config_new_option( jabber_config_file, jabber_config_section["color"], "message_join", "color", "color for text in join messages", "", 0, 0, "green", "green", 0, "", "", "", "", "", "") jabber_config_option["message_quit"] = weechat.config_new_option( jabber_config_file, jabber_config_section["color"], "message_quit", "color", "color for text in quit messages", "", 0, 0, "red", "red", 0, "", "", "", "", "", "") # server jabber_config_section["server"] = weechat.config_new_section( jabber_config_file, "server", 0, 0, "jabber_config_server_read_cb", "", "jabber_config_server_write_cb", "", "", "", "", "", "", "") if not jabber_config_section["server"]: weechat.config_free(jabber_config_file) return jabber_config_section["jid_aliases"] = weechat.config_new_section( jabber_config_file, "jid_aliases", 0, 0, "jabber_config_jid_aliases_read_cb", "", "jabber_config_jid_aliases_write_cb", "", "", "", "", "", "", "") if not jabber_config_section["jid_aliases"]: weechat.config_free(jabber_config_file) return def jabber_config_reload_cb(data, config_file): """ Reload config file. """ return weechat.config_reload(config_file) def jabber_config_server_read_cb(data, config_file, section, option_name, value): """ Read server option in config file. """ global jabber_servers rc = weechat.WEECHAT_CONFIG_OPTION_SET_ERROR items = option_name.split(".", 1) if len(items) == 2: server = jabber_search_server_by_name(items[0]) if not server: server = Server(items[0]) jabber_servers.append(server) if server: rc = weechat.config_option_set(server.options[items[1]], value, 1) return rc def jabber_config_server_write_cb(data, config_file, section_name): """ Write server section in config file. """ global jabber_servers weechat.config_write_line(config_file, section_name, "") for server in jabber_servers: for name, option in sorted(server.options.items()): weechat.config_write_option(config_file, option) return weechat.WEECHAT_RC_OK def jabber_config_jid_aliases_read_cb(data, config_file, section, option_name, value): """ Read jid_aliases option in config file. """ global jabber_jid_aliases jabber_jid_aliases[option_name] = value option = weechat.config_new_option( config_file, section, option_name, "string", "jid alias", "", 0, 0, "", value, 0, "", "", "", "", "", "") if not option: return weechat.WEECHAT_CONFIG_OPTION_SET_ERROR return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED def jabber_config_jid_aliases_write_cb(data, config_file, section_name): """ Write jid_aliases section in config file. """ global jabber_jid_aliases weechat.config_write_line(config_file, section_name, "") for alias, jid in sorted(jabber_jid_aliases.items()): weechat.config_write_line(config_file, alias, jid) return weechat.WEECHAT_RC_OK def jabber_config_read(): """ Read jabber config file (jabber.conf). """ global jabber_config_file return weechat.config_read(jabber_config_file) def jabber_config_write(): """ Write jabber config file (jabber.conf). """ global jabber_config_file return weechat.config_write(jabber_config_file) def jabber_debug_enabled(): """ Return True if debug is enabled. """ global jabber_config_options if weechat.config_boolean(jabber_config_option["debug"]): return True return False def jabber_config_color(color): """ Return color code for a jabber color option. """ global jabber_config_option if color in jabber_config_option: return weechat.color(weechat.config_color(jabber_config_option[color])) return "" def ping_timeout_check_cb(server_name, option, value): global jabber_config_file, jabber_config_section ping_interval_option = weechat.config_search_option( jabber_config_file, jabber_config_section["server"], "%s.ping_interval" % (server_name) ) ping_interval = weechat.config_integer(ping_interval_option) if int(ping_interval) and int(value) >= int(ping_interval): weechat.prnt("", "\njabber: unable to update 'ping_timeout' for server %s" % (server_name)) weechat.prnt("", "jabber: to prevent multiple concurrent pings, ping_interval must be greater than ping_timeout") return weechat.WEECHAT_CONFIG_OPTION_SET_ERROR return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED def ping_interval_check_cb(server_name, option, value): global jabber_config_file, jabber_config_section ping_timeout_option = weechat.config_search_option( jabber_config_file, jabber_config_section["server"], "%s.ping_timeout" % (server_name) ) ping_timeout = weechat.config_integer(ping_timeout_option) if int(value) and int(ping_timeout) >= int(value): weechat.prnt("", "\njabber: unable to update 'ping_interval' for server %s" % (server_name)) weechat.prnt("", "jabber: to prevent multiple concurrent pings, ping_interval must be greater than ping_timeout") return weechat.WEECHAT_CONFIG_OPTION_SET_ERROR return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED # ================================[ servers ]================================= class Server: """ Class to manage a server: buffer, connection, send/recv data. """ def __init__(self, name, **kwargs): """ Init server """ global jabber_config_file, jabber_config_section, jabber_server_options self.name = name # create options (user can set them with /set) self.options = {} # if the value is provided, use it, otherwise use the default values = {} for option_name, props in jabber_server_options.items(): values[option_name] = props["default"] values['name'] = name values.update(**kwargs) for option_name, props in jabber_server_options.items(): self.options[option_name] = weechat.config_new_option( jabber_config_file, jabber_config_section["server"], self.name + "." + option_name, props["type"], props["desc"], props["string_values"], props["min"], props["max"], props["default"], values[option_name], 0, props["check_cb"], self.name, props["change_cb"], "", props["delete_cb"], "") # internal data self.jid = None self.client = None self.sock = None self.hook_fd = None self.buffer = "" self.chats = [] self.roster = None self.buddies = [] self.buddy = None self.ping_timer = None # weechat.hook_timer for sending pings self.ping_timeout_timer = None # weechat.hook_timer for monitoring ping timeout self.ping_up = False # Connection status as per pings. self.presence = xmpp.protocol.Presence() def option_string(self, option_name): """ Return a server option, as string. """ return weechat.config_string(self.options[option_name]) def option_boolean(self, option_name): """ Return a server option, as boolean. """ return weechat.config_boolean(self.options[option_name]) def option_integer(self, option_name): """ Return a server option, as string. """ return weechat.config_integer(self.options[option_name]) def connect(self): """ Connect to Jabber server. """ if not self.buffer: bufname = "%s.server.%s" % (SCRIPT_NAME, self.name) self.buffer = weechat.buffer_search("python", bufname) if not self.buffer: self.buffer = weechat.buffer_new(bufname, "jabber_buffer_input_cb", "", "jabber_buffer_close_cb", "") if self.buffer: weechat.buffer_set(self.buffer, "short_name", self.name) weechat.buffer_set(self.buffer, "localvar_set_type", "server") weechat.buffer_set(self.buffer, "localvar_set_server", self.name) weechat.buffer_set(self.buffer, "nicklist", "1") weechat.buffer_set(self.buffer, "nicklist_display_groups", "1") weechat.buffer_set(self.buffer, "display", "auto") self.disconnect() if not eval_expression(self.option_string("jid")): weechat.prnt(self.buffer, "%sjabber: JID must contain at least domain name" % weechat.prefix("error")) self.ping_up = False self.client = None return self.is_connected() self.buddy = Buddy(jid=eval_expression(self.option_string("jid")), server=self) server = self.option_string("server") port = self.option_integer("port") self.client = xmpp.Client(server=self.buddy.domain, debug=[]) conn = None server_tuple = None if server: if port: server_tuple = (server, port) else: server_tuple = (server) # self.client.connect() may produce a "socket.ssl() is deprecated" # warning. Since the code producing the warning is outside this script, # catch it and ignore. original_filters = warnings.filters[:] warnings.filterwarnings("ignore",category=DeprecationWarning) try: conn = self.client.connect(server=server_tuple) finally: warnings.filters = original_filters if conn: weechat.prnt(self.buffer, "jabber: connection ok with %s" % conn) res = self.buddy.resource if not res: res = "WeeChat" auth = self.client.auth(self.buddy.username, eval_expression(self.option_string("password")), res) if auth: weechat.prnt(self.buffer, "jabber: authentication ok (using %s)" % auth) self.roster = self.client.getRoster() self.client.RegisterHandler("presence", self.presence_handler) self.client.RegisterHandler("iq", self.iq_handler) self.client.RegisterHandler("message", self.message_handler) self.client.sendInitPresence(requestRoster=1) self.sock = self.client.Connection._sock.fileno() self.hook_fd = weechat.hook_fd(self.sock, 1, 0, 0, "jabber_fd_cb", "") weechat.buffer_set(self.buffer, "highlight_words", self.buddy.username) weechat.buffer_set(self.buffer, "localvar_set_nick", self.buddy.username); hook_away = weechat.hook_command_run("/away -all*", "jabber_away_command_run_cb", "") # setting initial presence priority = weechat.config_integer(self.options['priority']) self.set_presence(show="",priority=priority) self.ping_up = True else: weechat.prnt(self.buffer, "%sjabber: could not authenticate" % weechat.prefix("error")) self.ping_up = False self.client = None else: weechat.prnt(self.buffer, "%sjabber: could not connect" % weechat.prefix("error")) self.ping_up = False self.client = None return self.is_connected() def is_connected(self): """Return connect status""" if not self.client or not self.client.isConnected(): return False else: return True def add_chat(self, buddy): """Create a chat buffer for a buddy""" chat = Chat(self, buddy, switch_to_buffer=False) self.chats.append(chat) return chat def add_buddy(self, jid): """ Add a new buddy """ self.client.Roster.Authorize(jid) self.client.Roster.Subscribe(jid) def del_buddy(self, jid): """ Remove a buddy and/or deny authorization request """ self.client.Roster.Unauthorize(jid) self.client.Roster.Unsubscribe(jid) def print_debug_server(self, message): """ Print debug message on server buffer. """ if jabber_debug_enabled(): weechat.prnt(self.buffer, "%sjabber: %s" % (weechat.prefix("network"), message)) def print_debug_handler(self, handler_name, node): """ Print debug message for a handler on server buffer. """ self.print_debug_server("%s_handler, xml message:\n%s" % (handler_name, node.__str__(fancy=True).encode("utf-8"))) def print_error(self, message): """ Print error message on server buffer. """ if jabber_debug_enabled(): weechat.prnt(self.buffer, "%sjabber: %s" % (weechat.prefix("error"), message)) def presence_handler(self, conn, node): self.print_debug_handler("presence", node) buddy = self.search_buddy_list(node.getFrom().getStripped().encode("utf-8"), by='jid') if not buddy: buddy = self.add_buddy(jid=node.getFrom()) action='update' node_type = node.getType() if node_type in ["error", "unavailable"]: action='remove' if action == 'update': away = node.getShow() in ["away", "xa"] status = '' if node.getStatus(): status = node.getStatus().encode("utf-8") if self.roster: name = self.roster.getName(buddy.bare_jid) if name: buddy.set_name(name.encode("utf-8")) buddy.set_status(status=status, away=away) self.update_nicklist(buddy=buddy, action=action) return def iq_handler(self, conn, node): """ Receive iq message. """ self.print_debug_handler("iq", node) #weechat.prnt(self.buffer, "jabber: iq handler") if node.getFrom() == self.buddy.domain: # type='result' => pong from server # type='error' => error message from server # The ping_up is set True on an error message to handle cases where # the ping feature is not implemented on a server. It's a bit of a # hack, but if we can receive an error from the server, we assume # the connection to the server is up. if node.getType() in ['result', 'error']: self.delete_ping_timeout_timer() # Disable the timeout feature self.ping_up = True if not self.client.isConnected() and weechat.config_boolean(self.options['autoreconnect']): self.connect() def message_handler(self, conn, node): """ Receive message. """ self.print_debug_handler("message", node) node_type = node.getType() if node_type not in ["message", "chat", None]: self.print_error("unknown message type: '%s'" % node_type) return jid = node.getFrom() body = node.getBody() if not jid or not body: return buddy = self.search_buddy_list(self.stringify_jid(jid), by='jid') if not buddy: buddy = self.add_buddy(jid=jid) # If a chat buffer exists for the buddy, receive the message with that # buffer even if private is off. The buffer may have been created with # /jchat. recv_object = self if not buddy.chat and weechat.config_boolean(self.options['private']): self.add_chat(buddy) if buddy.chat: recv_object = buddy.chat recv_object.recv_message(buddy, body.encode("utf-8")) def recv(self): """ Receive something from Jabber server. """ if not self.client: return try: self.client.Process(1) except xmpp.protocol.StreamError as e: weechat.prnt('', '%s: Error from server: %s' %(SCRIPT_NAME, e)) self.disconnect() if weechat.config_boolean(self.options['autoreconnect']): autoreconnect_delay = 30 weechat.command('', '/wait %s /%s connect %s' % (autoreconnect_delay, SCRIPT_COMMAND, self.name)) def recv_message(self, buddy, message): """ Receive a message from buddy. """ weechat.prnt_date_tags(self.buffer, 0, "notify_private,nick_%s,prefix_nick_%s,log1" % (buddy.alias, weechat.config_string(weechat.config_get("weechat.color.chat_nick_other"))), "%s%s\t%s" % (weechat.color("chat_nick_other"), buddy.alias, message)) def print_status(self, nickname, status): """ Print a status in server window and in chat. """ weechat.prnt_date_tags(self.buffer, 0, "no_highlight", "%s%s has status %s" % (weechat.prefix("action"), nickname, status)) for chat in self.chats: if nickname in chat.buddy.alias: chat.print_status(status) break def send_message(self, buddy, message): """ Send a message to buddy. The buddy argument can be either a jid string, eg username@domain.tld/resource or a Buddy object instance. """ recipient = buddy if isinstance(buddy, Buddy): recipient = buddy.jid if not self.ping_up: weechat.prnt(self.buffer, "%sjabber: unable to send message, connection is down" % weechat.prefix("error")) return if self.client: msg = xmpp.protocol.Message(to=recipient, body=message, typ='chat') self.client.send(msg) def send_message_from_input(self, input=''): """ Send a message from input text on server buffer. """ # Input must be of format "name: message" where name is a jid, bare_jid # or alias. The colon can be replaced with a comma as well. # Split input into name and message. if not re.compile(r'.+[:,].+').match(input): weechat.prnt(self.buffer, "%sjabber: %s" % (weechat.prefix("network"), "Invalid send format. Use jid: message" )) return name, message = re.split('[:,]', input, maxsplit=1) buddy = self.search_buddy_list(name, by='alias') if not buddy: weechat.prnt(self.buffer, "%sjabber: Invalid jid: %s" % (weechat.prefix("network"), name)) return # Send activity indicates user is no longer away, set it so if self.buddy and self.buddy.away: self.set_away('') self.send_message(buddy=buddy, message=message) try: sender = self.buddy.alias except: sender = self.jid weechat.prnt_date_tags(self.buffer, 0, "notify_none,no_highlight,nick_%s,prefix_nick_%s,log1" % (sender, weechat.config_string(weechat.config_get("weechat.color.chat_nick_self"))), "%s%s\t%s" % (weechat.color("chat_nick_self"), sender, message.strip())) def set_away(self, message): """ Set/unset away on server. If a message is provided, status is set to 'away'. If no message, then status is set to 'online'. """ if message: show = "xa" status = message priority = weechat.config_integer(self.options['away_priority']) self.buddy.set_status(away=True, status=message) else: show = "" status = None priority = weechat.config_integer(self.options['priority']) self.buddy.set_status(away=False) self.set_presence(show, status, priority) def set_presence(self, show=None, status=None, priority=None): if not show == None: self.presence.setShow(show) if not status == None: self.presence.setStatus(status) if not priority == None: self.presence.setPriority(priority) self.client.send(self.presence) def add_buddy(self, jid=None): buddy = Buddy(jid=jid, server=self) buddy.resource = buddy.resource.encode("utf-8") self.buddies.append(buddy) return buddy def display_buddies(self): """ Display buddies. """ weechat.prnt(self.buffer, "") weechat.prnt(self.buffer, "Buddies:") len_max = { 'alias': 5, 'jid': 5 } lines = [] for buddy in sorted(self.buddies, key=lambda x: x.jid.getStripped().encode('utf-8')): alias = '' if buddy.alias != buddy.bare_jid: alias = buddy.alias buddy_jid_string = buddy.jid.getStripped().encode('utf-8') lines.append( { 'jid': buddy_jid_string, 'alias': alias, 'status': buddy.away_string(), }) if len(alias) > len_max['alias']: len_max['alias'] = len(alias) if len(buddy_jid_string) > len_max['jid']: len_max['jid'] = len(buddy_jid_string) prnt_format = " %s%-" + str(len_max['jid']) + "s %-" + str(len_max['alias']) + "s %s" weechat.prnt(self.buffer, prnt_format % ('', 'JID', 'Alias', 'Status')) for line in lines: weechat.prnt(self.buffer, prnt_format % (weechat.color("chat_nick"), line['jid'], line['alias'], line['status'], )) def stringify_jid(self, jid, wresource=1): """ Serialise JID into string. Args: jid: xmpp.protocol.JID, JID instance to serialize Notes: Method is based on original JID.__str__ but with hack to allow non-ascii in resource names. """ if jid.node: jid_str = jid.node + '@' + jid.domain else: jid_str = jid.domain if wresource and jid.resource: # concatenate jid with resource delimiter first and encode them # into utf-8, else it will raise UnicodeException becaouse of # slash character :(( return (jid_str + '/').encode("utf-8") + jid.resource.encode("utf-8") return jid_str.encode("utf-8") def search_buddy_list(self, name, by='jid'): """ Search for a buddy by name. Args: name: string, the buddy name to search, eg the jid or alias by: string, either 'alias' or 'jid', determines which Buddy property to match on, default 'jid' Notes: If the 'by' parameter is set to 'jid', the search matches on all Buddy object jid properties, followed by all bare_jid properties. Once a match is found it is returned. If the 'by' parameter is set to 'alias', the search matches on all Buddy object alias properties. Generally, set the 'by' parameter to 'jid' when the jid is provided from a server, for example from a received message. Set 'by' to 'alias' when the jid is provided by the user. """ if by == 'jid': for buddy in self.buddies: if self.stringify_jid(buddy.jid) == name: return buddy for buddy in self.buddies: if buddy.bare_jid == name: return buddy else: for buddy in self.buddies: if buddy.alias == name: return buddy return None def update_nicklist(self, buddy=None, action=None): """Update buddy in nicklist Args: buddy: Buddy object instance action: string, one of 'update' or 'remove' """ if not buddy: return if not action in ['remove', 'update']: return ptr_nick_gui = weechat.nicklist_search_nick(self.buffer, "", buddy.alias) weechat.nicklist_remove_nick(self.buffer, ptr_nick_gui) msg = '' prefix = '' color = '' away = '' if action == 'update': nick_color = "bar_fg" if buddy.away: nick_color = "weechat.color.nicklist_away" weechat.nicklist_add_nick(self.buffer, "", buddy.alias, nick_color, "", "", 1) if not ptr_nick_gui: msg = 'joined' prefix = 'join' color = 'message_join' away = buddy.away_string() if action == 'remove': msg = 'quit' prefix = 'quit' color = 'message_quit' if msg: weechat.prnt(self.buffer, "%s%s%s%s has %s %s" % (weechat.prefix(prefix), weechat.color("chat_nick"), buddy.alias, jabber_config_color(color), msg, away)) return def add_ping_timer(self): if self.ping_timer: self.delete_ping_timer() if not self.option_integer('ping_interval'): return self.ping_timer = weechat.hook_timer( self.option_integer('ping_interval') * 1000, 0, 0, "jabber_ping_timer", self.name) return def delete_ping_timer(self): if self.ping_timer: weechat.unhook(self.ping_timer) self.ping_time = None return def add_ping_timeout_timer(self): if self.ping_timeout_timer: self.delete_ping_timeout_timer() if not self.option_integer('ping_timeout'): return self.ping_timeout_timer = weechat.hook_timer( self.option_integer('ping_timeout') * 1000, 0, 1, "jabber_ping_timeout_timer", self.name) return def delete_ping_timeout_timer(self): if self.ping_timeout_timer: weechat.unhook(self.ping_timeout_timer) self.ping_timeout_timer = None return def ping(self): if not self.is_connected(): if not self.connect(): return iq = xmpp.protocol.Iq(to=self.buddy.domain, typ='get') iq.addChild( name= "ping", namespace = "urn:xmpp:ping" ) id = self.client.send(iq) self.print_debug_handler("ping", iq) self.add_ping_timeout_timer() return def ping_time_out(self): self.delete_ping_timeout_timer() self.ping_up = False # A ping timeout indicates a server connection problem. Disconnect # completely. try: self.client.disconnected() except IOError: # An IOError is raised by the default DisconnectHandler pass self.disconnect() return def disconnect(self): """ Disconnect from Jabber server. """ if self.hook_fd != None: weechat.unhook(self.hook_fd) self.hook_fd = None if self.client != None: #if self.client.isConnected(): # self.client.disconnect() self.client = None self.jid = None self.sock = None self.buddy = None weechat.nicklist_remove_all(self.buffer) def close_buffer(self): """ Close server buffer. """ if self.buffer != "": weechat.buffer_close(self.buffer) self.buffer = "" def delete(self, deleteOptions=False): """ Delete server. """ for chat in self.chats: chat.delete() self.delete_ping_timer() self.delete_ping_timeout_timer() self.disconnect() self.close_buffer() if deleteOptions: for name, option in self.options.items(): weechat.config_option_free(option) def eval_expression(option_name): """ Return a evaluated expression """ if int(version) >= 0x00040200: return weechat.string_eval_expression(option_name,{},{},{}) else: return option_name def jabber_search_server_by_name(name): """ Search a server by name. """ global jabber_servers for server in jabber_servers: if server.name == name: return server return None def jabber_search_context(buffer): """ Search a server / chat for a buffer. """ global jabber_servers context = { "server": None, "chat": None } for server in jabber_servers: if server.buffer == buffer: context["server"] = server return context for chat in server.chats: if chat.buffer == buffer: context["server"] = server context["chat"] = chat return context return context def jabber_search_context_by_name(server_name): """Search for buffer given name of server. """ bufname = "%s.server.%s" % (SCRIPT_NAME, server_name) return jabber_search_context(weechat.buffer_search("python", bufname)) # =================================[ chats ]================================== class Chat: """ Class to manage private chat with buddy or MUC. """ def __init__(self, server, buddy, switch_to_buffer): """ Init chat """ self.server = server self.buddy = buddy buddy.chat = self bufname = "%s.%s.%s" % (SCRIPT_NAME, server.name, self.buddy.alias) self.buffer = weechat.buffer_search("python", bufname) if not self.buffer: self.buffer = weechat.buffer_new(bufname, "jabber_buffer_input_cb", "", "jabber_buffer_close_cb", "") self.buffer_title = self.buddy.alias if self.buffer: weechat.buffer_set(self.buffer, "title", self.buffer_title) weechat.buffer_set(self.buffer, "short_name", self.buddy.alias) weechat.buffer_set(self.buffer, "localvar_set_type", "private") weechat.buffer_set(self.buffer, "localvar_set_server", server.name) weechat.buffer_set(self.buffer, "localvar_set_channel", self.buddy.alias) weechat.hook_signal_send("logger_backlog", weechat.WEECHAT_HOOK_SIGNAL_POINTER, self.buffer) if switch_to_buffer: weechat.buffer_set(self.buffer, "display", "auto") def recv_message(self, buddy, message): """ Receive a message from buddy. """ if buddy.alias != self.buffer_title: self.buffer_title = buddy.alias weechat.buffer_set(self.buffer, "title", "%s" % self.buffer_title) weechat.prnt_date_tags(self.buffer, 0, "notify_private,nick_%s,prefix_nick_%s,log1" % (buddy.alias, weechat.config_string(weechat.config_get("weechat.color.chat_nick_other"))), "%s%s\t%s" % (weechat.color("chat_nick_other"), buddy.alias, message)) def send_message(self, message): """ Send message to buddy. """ if not self.server.ping_up: weechat.prnt(self.buffer, "%sjabber: unable to send message, connection is down" % weechat.prefix("error")) return self.server.send_message(self.buddy, message) weechat.prnt_date_tags(self.buffer, 0, "notify_none,no_highlight,nick_%s,prefix_nick_%s,log1" % (self.server.buddy.alias, weechat.config_string(weechat.config_get("weechat.color.chat_nick_self"))), "%s%s\t%s" % (weechat.color("chat_nick_self"), self.server.buddy.alias, message)) def print_status(self, status): """ Print a status message in chat. """ weechat.prnt(self.buffer, "%s%s has status %s" % (weechat.prefix("action"), self.buddy.alias, status)) def close_buffer(self): """ Close chat buffer. """ if self.buffer != "": weechat.buffer_close(self.buffer) self.buffer = "" def delete(self): """ Delete chat. """ self.close_buffer() # =================================[ buddies ]================================== class Buddy: """ Class to manage buddies. """ def __init__(self, jid=None, chat=None, server=None ): """ Init buddy Args: jid: xmpp.protocol.JID object instance or string chat: Chat object instance server: Server object instance The jid argument can be provided either as a xmpp.protocol.JID object instance or as a string, eg "username@domain.tld/resource". If a string is provided, it is converted and stored as a xmpp.protocol.JID object instance. """ # The jid argument of xmpp.protocol.JID can be either a string or a # xmpp.protocol.JID object instance itself. self.jid = xmpp.protocol.JID(jid=jid) self.chat = chat self.server = server self.bare_jid = '' self.username = '' self.name = '' self.domain = '' self.resource = '' self.alias = '' self.away = True self.status = '' self.parse_jid() self.set_alias() return def away_string(self): """ Return a string with away and status, with color codes. """ if not self: return '' if not self.away: return '' str_colon = ": " if not self.status: str_colon = "" return "%s(%saway%s%s%s)" % (weechat.color("chat_delimiters"), weechat.color("chat"), str_colon, self.status.replace("\n", " "), weechat.color("chat_delimiters")) def parse_jid(self): """Parse the jid property. The table shows how the jid is parsed and which properties are updated. Property Value jid myuser@mydomain.tld/myresource bare_jid myuser@mydomain.tld username myuser domain mydomain.tld resource myresource """ if not self.jid: return self.bare_jid = self.jid.getStripped().encode("utf-8") self.username = self.jid.getNode() self.domain = self.jid.getDomain() self.resource = self.jid.getResource() return def set_alias(self): """Set the buddy alias. If an alias is defined in jabber_jid_aliases, it is used. Otherwise the alias is set to self.bare_jid or self.name if it exists. """ self.alias = self.bare_jid if not self.bare_jid: self.alias = '' if self.name: self.alias = self.name global jabber_jid_aliases for alias, jid in jabber_jid_aliases.items(): if jid == self.bare_jid: self.alias = alias break return def set_name(self, name=''): self.name = name self.set_alias() return def set_status(self, away=True, status=''): """Set the buddy status. Two properties define the buddy status. away - boolean, indicates whether the buddy is away or not. status - string, a message indicating the away status, eg 'in a meeting' Comparable to xmpp presence element. """ if not away and not status: status = 'online' # If the status has changed print a message on the server buffer if self.away != away or self.status != status: self.server.print_status(self.alias, status) self.away = away self.status = status return # ================================[ commands ]================================ def jabber_hook_commands_and_completions(): """ Hook commands and completions. """ weechat.hook_command(SCRIPT_COMMAND, "Manage Jabber servers", "list || add [[:]]" " || connect|disconnect|del [] || alias [add|del ]" " || away [] || buddies || priority []" " || status [] || presence [online|chat|away|xa|dnd]" " || debug || set []", " list: list servers and chats\n" " add: add a server\n" " connect: connect to server using password\n" "disconnect: disconnect from server\n" " del: delete server\n" " alias: manage jid aliases\n" " away: set away with a message (if no message, away is unset)\n" " priority: set priority\n" " status: set status message\n" " presence: set presence status\n" " buddies: display buddies on server\n" " debug: toggle jabber debug on/off (for all servers)\n" "\n" "Without argument, this command lists servers and chats.\n" "\n" "Examples:\n" " Add a server: /jabber add myserver user@server.tld password\n" " Add gtalk server: /jabber add myserver user@gmail.com password talk.google.com:5223\n" " Connect to server: /jabber connect myserver\n" " Disconnect: /jabber disconnect myserver\n" " Delete server: /jabber del myserver\n" "\n" "Aliases:\n" " List aliases: /jabber alias \n" " Add an alias: /jabber alias add alias_name jid\n" " Delete an alias: /jabber alias del alias_name\n" "\n" "Other jabber commands:\n" " Chat with a buddy (pv buffer): /jchat\n" " Add buddy to roster: /invite\n" " Remove buddy from roster: /kick\n" " Send message to buddy: /jmsg", "list %(jabber_servers)" " || add %(jabber_servers)" " || connect %(jabber_servers)" " || disconnect %(jabber_servers)" " || del %(jabber_servers)" " || alias add|del %(jabber_jid_aliases)" " || away" " || priority" " || status" " || presence online|chat|away|xa|dnd" " || buddies" " || debug", "jabber_cmd_jabber", "") weechat.hook_command("jchat", "Chat with a Jabber buddy", "", "buddy: buddy id", "", "jabber_cmd_jchat", "") weechat.hook_command("jmsg", "Send a messge to a buddy", "[-server ] ", "server: name of jabber server buddy is on\n" " buddy: buddy id\n" " text: text to send", "", "jabber_cmd_jmsg", "") weechat.hook_command("invite", "Add a buddy to your roster", "", "buddy: buddy id", "", "jabber_cmd_invite", "") weechat.hook_command("kick", "Remove a buddy from your roster, or deny auth", "", "buddy: buddy id", "", "jabber_cmd_kick", "") weechat.hook_completion("jabber_servers", "list of jabber servers", "jabber_completion_servers", "") weechat.hook_completion("jabber_jid_aliases", "list of jabber jid aliases", "jabber_completion_jid_aliases", "") def jabber_list_servers_chats(name): """ List servers and chats. """ global jabber_servers weechat.prnt("", "") if len(jabber_servers) > 0: weechat.prnt("", "jabber servers:") for server in jabber_servers: if name == "" or server.name.find(name) >= 0: conn_server = '' if server.option_string("server"): conn_server = ':'.join( (server.option_string("server"), server.option_string("port"))) connected = "" if server.sock >= 0: connected = "(connected)" weechat.prnt("", " %s - %s %s %s" % (server.name, eval_expression(server.option_string("jid")), conn_server, connected)) for chat in server.chats: weechat.prnt("", " chat with %s" % (chat.buddy)) else: weechat.prnt("", "jabber: no server defined") def jabber_cmd_jabber(data, buffer, args): """ Command '/jabber'. """ global jabber_servers, jabber_config_option if args == "" or args == "list": jabber_list_servers_chats("") else: argv = args.split(" ") argv1eol = "" pos = args.find(" ") if pos > 0: argv1eol = args[pos+1:] if argv[0] == "list": jabber_list_servers_chats(argv[1]) elif argv[0] == "add": if len(argv) >= 4: server = jabber_search_server_by_name(argv[1]) if server: weechat.prnt("", "jabber: server '%s' already exists" % argv[1]) else: kwargs = {'jid': argv[2], 'password': argv[3]} if len(argv) > 4: conn_server, _, conn_port = argv[4].partition(':') if conn_port and not conn_port.isdigit(): weechat.prnt("", "jabber: error, invalid port, digits only") return weechat.WEECHAT_RC_OK if conn_server: kwargs['server'] = conn_server if conn_port: kwargs['port'] = conn_port server = Server(argv[1], **kwargs) jabber_servers.append(server) weechat.prnt("", "jabber: server '%s' created" % argv[1]) else: weechat.prnt("", "jabber: unable to add server, missing arguments") weechat.prnt("", "jabber: usage: /jabber add name jid password [server[:port]]") elif argv[0] == "alias": alias_command = AliasCommand(buffer, argv=argv[1:]) alias_command.run() elif argv[0] == "connect": server = None if len(argv) >= 2: server = jabber_search_server_by_name(argv[1]) if not server: weechat.prnt("", "jabber: server '%s' not found" % argv[1]) else: context = jabber_search_context(buffer) if context["server"]: server = context["server"] if server: if weechat.config_boolean(server.options['autoreconnect']): server.ping() # This will connect and update ping status server.add_ping_timer() else: server.connect() elif argv[0] == "disconnect": server = None if len(argv) >= 2: server = jabber_search_server_by_name(argv[1]) if not server: weechat.prnt("", "jabber: server '%s' not found" % argv[1]) else: context = jabber_search_context(buffer) if context["server"]: server = context["server"] context = jabber_search_context(buffer) if server: server.delete_ping_timer() server.disconnect() elif argv[0] == "del": if len(argv) >= 2: server = jabber_search_server_by_name(argv[1]) if server: server.delete(deleteOptions=True) jabber_servers.remove(server) weechat.prnt("", "jabber: server '%s' deleted" % argv[1]) else: weechat.prnt("", "jabber: server '%s' not found" % argv[1]) elif argv[0] == "send": if len(argv) >= 3: context = jabber_search_context(buffer) if context["server"]: buddy = context['server'].search_buddy_list(argv[1], by='alias') message = ' '.join(argv[2:]) context["server"].send_message(buddy, message) elif argv[0] == "read": jabber_config_read() elif argv[0] == "away": context = jabber_search_context(buffer) if context["server"]: context["server"].set_away(argv1eol) elif argv[0] == "priority": context = jabber_search_context(buffer) if context["server"]: if len(argv) == 1: weechat.prnt("", "jabber: priority = %d" % int(context["server"].presence.getPriority())) elif len(argv) == 2 and argv[1].isdigit(): context["server"].set_presence(priority=int(argv[1])) else: weechat.prnt("", "jabber: you need to specify priority as positive integer between 0 and 65535") elif argv[0] == "status": context = jabber_search_context(buffer) if context["server"]: if len(argv) == 1: weechat.prnt("", "jabber: status = %s" % context["server"].presence.getStatus()) else: context["server"].set_presence(status=argv1eol) elif argv[0] == "presence": context = jabber_search_context(buffer) if context["server"]: if len(argv) == 1: show = context["server"].presence.getShow() if show == "": show = "online" weechat.prnt("", "jabber: presence = %s" % show) elif not re.match(r'^(?:online|chat|away|xa|dnd)$', argv[1]): weechat.prnt("", "jabber: Presence should be one of: online, chat, away, xa, dnd") else: if argv[1] == "online": show = "" else: show = argv[1] context["server"].set_presence(show=show) elif argv[0] == "buddies": context = jabber_search_context(buffer) if context["server"]: context["server"].display_buddies() elif argv[0] == "debug": weechat.config_option_set(jabber_config_option["debug"], "toggle", 1) if jabber_debug_enabled(): weechat.prnt("", "jabber: debug is now ON") else: weechat.prnt("", "jabber: debug is now off") else: weechat.prnt("", "jabber: unknown action") return weechat.WEECHAT_RC_OK def jabber_cmd_jchat(data, buffer, args): """ Command '/jchat'. """ if args: context = jabber_search_context(buffer) if context["server"]: buddy = context["server"].search_buddy_list(args, by='alias') if not buddy: buddy = context["server"].add_buddy(jid=args) if not buddy.chat: context["server"].add_chat(buddy) weechat.buffer_set(buddy.chat.buffer, "display", "auto") return weechat.WEECHAT_RC_OK def jabber_cmd_jmsg(data, buffer, args): """ Command '/jmsg'. """ if args: argv = args.split() if len(argv) < 2: return weechat.WEECHAT_RC_OK if argv[0] == '-server': context = jabber_search_context_by_name(argv[1]) recipient = argv[2] message = " ".join(argv[3:]) else: context = jabber_search_context(buffer) recipient = argv[0] message = " ".join(argv[1:]) if context["server"]: buddy = context['server'].search_buddy_list(recipient, by='alias') context["server"].send_message(buddy, message) return weechat.WEECHAT_RC_OK def jabber_cmd_invite(data, buffer, args): """ Command '/invite'. """ if args: context = jabber_search_context(buffer) if context["server"]: context["server"].add_buddy(args) return weechat.WEECHAT_RC_OK def jabber_cmd_kick(data, buffer, args): """ Command '/kick'. """ if args: context = jabber_search_context(buffer) if context["server"]: context["server"].del_buddy(args) return weechat.WEECHAT_RC_OK def jabber_away_command_run_cb(data, buffer, command): """ Callback called when /away -all command is run """ global jabber_servers words = command.split(None, 2) if len(words) < 2: return message = '' if len(words) > 2: message = words[2] for server in jabber_servers: server.set_away(message) return weechat.WEECHAT_RC_OK class AliasCommand(object): """Class representing a jabber alias command, ie /jabber alias ...""" def __init__(self, buffer, argv=None): """ Args: bufffer: the weechat buffer the command was run in argv: list, the arguments provided with the command. Example, if the command is "/jabber alias add abc abc@server.tld" argv = ['add', 'abc', 'abc@server.tld'] """ self.buffer = buffer self.argv = [] if argv: self.argv = argv self.action = '' self.jid = '' self.alias = '' self.parse() return def add(self): """Run a "/jabber alias add" command""" global jabber_jid_aliases if not self.alias or not self.jid: weechat.prnt("", "\njabber: unable to add alias, missing arguments") weechat.prnt("", "jabber: usage: /jabber alias add alias_name jid") return # Restrict the character set of aliases. The characters must be writable to # config file. invalid_re = re.compile(r'[^a-zA-Z0-9\[\]\\\^_\-{|}@\.]') if invalid_re.search(self.alias): weechat.prnt("", "\njabber: invalid alias: %s" % self.alias) weechat.prnt("", "jabber: use only characters: a-z A-Z 0-9 [ \ ] ^ _ - { | } @ .") return # Ensure alias and jid are reasonable length. max_len = 64 if len(self.alias) > max_len: weechat.prnt("", "\njabber: invalid alias: %s" % self.alias) weechat.prnt("", "jabber: must be no more than %s characters long" % max_len) return if len(self.jid) > max_len: weechat.prnt("", "\njabber: invalid jid: %s" % self.jid) weechat.prnt("", "jabber: must be no more than %s characters long" % max_len) return jid = self.jid.encode("utf-8") alias = self.alias.encode("utf-8") if alias in jabber_jid_aliases.keys(): weechat.prnt("", "\njabber: unable to add alias: %s" % (alias)) weechat.prnt("", "jabber: alias already exists, delete first") return if jid in jabber_jid_aliases.values(): weechat.prnt("", "\njabber: unable to add alias: %s" % (alias)) for a, j in jabber_jid_aliases.items(): if j == jid: weechat.prnt("", "jabber: jid '%s' is already aliased as '%s', delete first" % (j, a)) break jabber_jid_aliases[alias] = jid self.alias_reset(jid) return def alias_reset(self, jid): """Reset objects related to the jid modified by an an alias command Update any existing buddy objects, server nicklists, and chat objects that may be using the buddy with the provided jid. """ global jabber_servers for server in jabber_servers: buddy = server.search_buddy_list(jid, by='jid') if not buddy: continue server.update_nicklist(buddy=buddy, action='remove') buddy.set_alias() server.update_nicklist(buddy=buddy, action='update') if buddy.chat: switch_to_buffer = False if buddy.chat.buffer == self.buffer: switch_to_buffer = True buddy.chat.delete() new_chat = server.add_chat(buddy) if switch_to_buffer: weechat.buffer_set(new_chat.buffer, "display", "auto") return def delete(self): """Run a "/jabber alias del" command""" global jabber_jid_aliases if not self.alias: weechat.prnt("", "\njabber: unable to delete alias, missing arguments") weechat.prnt("", "jabber: usage: /jabber alias del alias_name") return if not self.alias in jabber_jid_aliases: weechat.prnt("", "\njabber: unable to delete alias '%s', not found" % (self.alias)) return jid = jabber_jid_aliases[self.alias] del jabber_jid_aliases[self.alias] self.alias_reset(jid) return def list(self): """Run a "/jabber alias" command to list aliases""" global jabber_jid_aliases weechat.prnt("", "") if len(jabber_jid_aliases) <= 0: weechat.prnt("", "jabber: no aliases defined") return weechat.prnt("", "jabber jid aliases:") len_alias = 5 len_jid = 5 for alias, jid in jabber_jid_aliases.items(): if len_alias < len(alias): len_alias = len(alias) if len_jid < len(jid): len_jid = len(jid) prnt_format = " %-" + str(len_alias) + "s %-" + str(len_jid) + "s" weechat.prnt("", prnt_format % ('Alias', 'JID')) for alias, jid in sorted(jabber_jid_aliases.items()): weechat.prnt("", prnt_format % (alias, jid)) return def parse(self): """Parse the alias command into components""" if len(self.argv) <= 0: return self.action = self.argv[0] if len(self.argv) > 1: # Pad argv list to prevent IndexError exceptions while len(self.argv) < 3: self.argv.append('') self.alias = self.argv[1] self.jid = self.argv[2] return def run(self): """Execute the alias command.""" if self.action == 'add': self.add() elif self.action == 'del': self.delete() self.list() return def jabber_completion_servers(data, completion_item, buffer, completion): """ Completion with jabber server names. """ global jabber_servers for server in jabber_servers: weechat.hook_completion_list_add(completion, server.name, 0, weechat.WEECHAT_LIST_POS_SORT) return weechat.WEECHAT_RC_OK def jabber_completion_jid_aliases(data, completion_item, buffer, completion): """ Completion with jabber alias names. """ global jabber_jid_aliases for alias, jid in sorted(jabber_jid_aliases.items()): weechat.hook_completion_list_add(completion, alias, 0, weechat.WEECHAT_LIST_POS_SORT) return weechat.WEECHAT_RC_OK # ==================================[ fd ]==================================== def jabber_fd_cb(data, fd): """ Callback for reading socket. """ global jabber_servers for server in jabber_servers: if server.sock == int(fd): server.recv() return weechat.WEECHAT_RC_OK # ================================[ buffers ]================================= def jabber_buffer_input_cb(data, buffer, input_data): """ Callback called for input data on a jabber buffer. """ context = jabber_search_context(buffer) if context["server"] and context["chat"]: context["chat"].send_message(input_data) elif context["server"]: if input_data == "buddies" or "buddies".startswith(input_data): context["server"].display_buddies() else: context["server"].send_message_from_input(input=input_data) return weechat.WEECHAT_RC_OK def jabber_buffer_close_cb(data, buffer): """ Callback called when a jabber buffer is closed. """ context = jabber_search_context(buffer) if context["server"] and context["chat"]: if context["chat"].buddy: context["chat"].buddy.chat = None context["chat"].buffer = "" context["server"].chats.remove(context["chat"]) elif context["server"]: context["server"].buffer = "" return weechat.WEECHAT_RC_OK # ==================================[ timers ]================================== def jabber_ping_timeout_timer(server_name, remaining_calls): server = jabber_search_server_by_name(server_name) if server: server.ping_time_out() return weechat.WEECHAT_RC_OK def jabber_ping_timer(server_name, remaining_calls): server = jabber_search_server_by_name(server_name) if server: server.ping() return weechat.WEECHAT_RC_OK # ==================================[ main ]================================== if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "jabber_unload_script", ""): version = weechat.info_get("version_number", "") or 0 jabber_hook_commands_and_completions() jabber_config_init() jabber_config_read() for server in jabber_servers: if weechat.config_boolean(server.options['autoreconnect']): server.ping() # This will connect and update ping status server.add_ping_timer() else: if weechat.config_boolean(server.options['autoconnect']): server.connect() # ==================================[ end ]=================================== def jabber_unload_script(): """ Function called when script is unloaded. """ global jabber_servers jabber_config_write() for server in jabber_servers: server.disconnect() server.delete() return weechat.WEECHAT_RC_OK weechat-scripts/python/autobump.py0000644000175100017510000001255415112622407016405 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2018 Daniel Kessler # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # This script bumps buffers when there is activity on them, # replicating the functionality of most mainstream chat programs. # Changelog: # Ver: 0.1.0 improve activity detection: # TODO: combine priorities of merged buffers import weechat SCRIPT_NAME = 'autobump' SCRIPT_AUTHOR = 'Daniel Kessler ' SCRIPT_VERSION = '0.1.0' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = 'Bump buffers upon activity.' DEFAULTS = { 'lowprio_buffers': ('', 'List of buffers to be sorted with low priority'), 'highprio_buffers': ('irc.server.*,core.weechat', 'List of buffers to be sorted with high priority'), 'tags': ('notify_message,notify_private,self_msg', 'List of message tags that are considered activity') } def on_autobump_command(data, buffer, args): argv = args.split() if len(argv) < 2 or argv[0] not in {'add', 'del'} or argv[1] not in {'high', 'low'}: return weechat.WEECHAT_RC_ERROR bufname = weechat.buffer_get_string(buffer, 'full_name') key = argv[1] + 'prio_buffers' buflist = weechat.config_get_plugin(key).split(',') if argv[0] == 'add': if bufname not in buflist: buflist.append(bufname) elif bufname in buflist: buflist.remove(bufname) else: return weechat.WEECHAT_RC_ERROR weechat.config_set_plugin(key, ','.join(buflist)) on_buffer_activity(buffer) return weechat.WEECHAT_RC_OK def get_buffers(): '''Get a list of all the buffers in weechat.''' hdata = weechat.hdata_get('buffer') buffer = weechat.hdata_get_list(hdata, "gui_buffers"); result = [] while buffer: number = weechat.hdata_integer(hdata, buffer, 'number') result.append((number, buffer)) buffer = weechat.hdata_pointer(hdata, buffer, 'next_buffer') return hdata, result def buffer_priority(buffer): '''Get a buffer's priority. Higher number means higher priority.''' lowprio_match = weechat.config_get_plugin('lowprio_buffers') if weechat.buffer_match_list(buffer, lowprio_match): return 0 highprio_match = weechat.config_get_plugin('highprio_buffers') if weechat.buffer_match_list(buffer, highprio_match): return 2 return 1 def on_buffer_activity(buffer): prio = buffer_priority(buffer) if prio == 2: weechat.buffer_set(buffer, 'number', '1') return hdata, buffers = get_buffers() for num, buf in reversed(buffers): if prio < buffer_priority(buf): weechat.buffer_set(buffer, 'number', str(num + 1)) return weechat.buffer_set(buffer, 'number', '1') def on_print(data, buffer, date, tags, displayed, highlight, prefix, message): if int(displayed): on_buffer_activity(buffer) return weechat.WEECHAT_RC_OK def on_buffer_open(data, signal, signal_data): on_buffer_activity(signal_data) return weechat.WEECHAT_RC_OK the_print_hook = None def update_hook(*args): global the_print_hook if the_print_hook: weechat.unhook(the_print_hook) value = weechat.config_get_plugin('tags') the_print_hook = weechat.hook_print('', value, '', 0, 'on_print', '') return weechat.WEECHAT_RC_OK command_description = r'''/autobump add high: Add the current buffer to the high priority list /autobump add low: Add the current buffer to the low priority list /autobump del high: Remove the current buffer from the high priority list /autobump del low: Remove the current buffer from the low priority list You can manually modify the high/low priority lists (for instance, with custom patterns) with /set var.plugins.python.autobump.highprio_buffers and /set var.plugins.python.autobump.lowprio_buffers. See /help filter for documentation on writing buffer lists. ''' command_completion = 'add high || add low || del high || del low' if __name__ == '__main__': if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): for option, value in DEFAULTS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) weechat.config_set_desc_plugin(option, value[1]) update_hook() weechat.hook_config('plugins.var.python.'+SCRIPT_NAME+'.tags', 'update_hook', '') weechat.hook_signal('buffer_opened', 'on_buffer_open', '') weechat.hook_command('autobump', command_description, '', '', command_completion, 'on_autobump_command', '') weechat-scripts/python/imap_status.py0000644000175100017510000001334115112622377017103 0ustar manumanu# -*- coding: utf-8 -*- # Copyright (c) 2009-2015 by xt # (this script requires WeeChat 0.4.2 or newer) # # History: # 2019-01-26, nils_2@freenode # version 0.9: make script python3 compatible # : remove option "message_color" and "separator_color" # 2016-05-07, Sebastien Helleu : # version 0.8: add options "mailbox_color", "separator", "separator_color", # remove extra colon in bar item content, use hook_process # to prevent any freeze in WeeChat >= 1.5 # 2015-01-09, nils_2 # version 0.7: use eval_expression() # 2010-07-12, TenOfTen # version 0.6: beautify notification area # 2010-03-17, xt # version 0.5: fix caching of return message # 2010-01-19, xt # version 0.4: only run check when timer expired # 2009-11-03, xt # version 0.3: multiple mailbox support # 2009-11-02, xt # version 0.2: remove the imap "client" buffer, just do the unread count # 2009-06-18, xt # version 0.1: initial release. # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # ''' Usage: put [imap] in your status bar items. (Or any other bar to your liking) "/set weechat.bar.status.items". ''' import imaplib as i import re import weechat as w SCRIPT_NAME = "imap_status" SCRIPT_AUTHOR = "xt " SCRIPT_VERSION = "0.9" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Bar item with unread imap messages count" WEECHAT_VERSION = 0 IMAP_UNREAD = '' # script options settings = { 'username': '', 'password': '', 'hostname': '', # gmail uses imap.gmail.com 'port': '993', 'mailboxes': 'INBOX', # comma separated list of mailboxes (gmail: "Inbox") 'message': '${color:default}Mail: ', 'mailbox_color': 'default', 'separator': '${color:default}, ', 'count_color': 'default', 'interval': '5', } def string_eval_expression(text): return w.string_eval_expression(text, {}, {}, {}) class Imap(object): """Simple helper class for interfacing with IMAP server.""" iRe = re.compile(br"UNSEEN (\d+)") conn = False def __init__(self): '''Connect and login.''' username = string_eval_expression(w.config_get_plugin('username')) password = string_eval_expression(w.config_get_plugin('password')) hostname = string_eval_expression(w.config_get_plugin('hostname')) port = int(w.config_get_plugin('port')) if username and password and hostname and port: M = i.IMAP4_SSL(hostname, port) M.login(username, password) self.conn = M def unreadCount(self, mailbox='INBOX'): if self.conn: unreadCount = int( self.iRe.search( self.conn.status(mailbox, "(UNSEEN)")[1][0]).group(1)) return unreadCount else: w.prnt('', 'Problem with IMAP connection. Please check settings.') return 0 def logout(self): if not self.conn: return try: self.conn.close() except Exception: self.conn.logout() def imap_get_unread(data): """Return the unread count.""" imap = Imap() if not w.config_get_plugin('message'): output = "" else: output = '%s' % ( string_eval_expression(w.config_get_plugin('message'))) any_with_unread = False mailboxes = w.config_get_plugin('mailboxes').split(',') count = [] for mailbox in mailboxes: mailbox = mailbox.strip() unreadCount = imap.unreadCount(mailbox) if unreadCount > 0: any_with_unread = True count.append('%s%s: %s%s' % ( w.color(w.config_get_plugin('mailbox_color')), mailbox, w.color(w.config_get_plugin('count_color')), unreadCount)) imap.logout() sep = '%s' % ( string_eval_expression(w.config_get_plugin('separator'))) output = output + sep.join(count) + w.color('reset') return output if any_with_unread else '' def imap_item_cb(data, item, window): return IMAP_UNREAD def imap_update_content(content): global IMAP_UNREAD if content != IMAP_UNREAD: IMAP_UNREAD = content w.bar_item_update('imap') def imap_process_cb(data, command, rc, out, err): if rc == 0: imap_update_content(out) return w.WEECHAT_RC_OK def imap_timer_cb(data, remaining_calls): """Timer callback to update imap bar item.""" if WEECHAT_VERSION >= 0x01050000: w.hook_process('func:imap_get_unread', 30 * 1000, 'imap_process_cb', '') else: imap_update_content(imap_get_unread(None)) # this can block WeeChat! return w.WEECHAT_RC_OK if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) WEECHAT_VERSION = int(w.info_get("version_number", "") or 0) w.bar_item_new('imap', 'imap_item_cb', '') imap_timer_cb(None, None) w.hook_timer( int(w.config_get_plugin('interval'))*1000*60, 0, 0, 'imap_timer_cb', '') weechat-scripts/python/weestats.py0000644000175100017510000001312115112622402016372 0ustar manumanu# -*- coding: utf-8 -*- # # weestats.py, version 0.2 for WeeChat version 0.3 # Latest development version: https://github.com/FiXato/weechat_scripts # # Inserts some statistics into your input field about the buffers/windows # you have open. # Example: 151 buffers (46 merged): 135 channels, 9 servers, 3 queries, # 1 script, 1 python, 1 perl, 1 core; 3 windows # ## History: # ### 2012-03-29: FiXato: # * version 0.1: initial release. # * Display a count of all the different buffers you have open. # * Display a count of all the open windows. # * version 0.2: Getting the splits. # * Displays the how many vertical and horizontal windows. # (not quite sure if my approximation is correct though..) # * Fixed possible memleak (forgot to free an infolist) ### 2015-05-02: arza: # * version 0.3: # * handle non-#-channels # * numerical sort for buffer info # * moved window split info to option -split # * simplified the output ### 2019-07-05: Sébastien Helleu: # * version 0.4: # * make script compatible with Python 3 # ## Acknowledgements: # * Sebastien "Flashcode" Helleu, for developing the kick-ass chat/IRC # client WeeChat # ## TODO: # - Add more statistics, such as: # - average and total history lines. # - average and total topic/title lengths # - how many are displayed in a window # ## Copyright (c) 2012 Filip H.F. "FiXato" Slagter, # # https://google.com/profiles/FiXato # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # from __future__ import print_function SCRIPT_NAME = "weestats" SCRIPT_AUTHOR = "Filip H.F. 'FiXato' Slagter " SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "MIT" SCRIPT_DESC = "Useless statistics about your open buffers and windows" SCRIPT_COMMAND = "weestats" SCRIPT_CLOSE_CB = "close_cb" import_ok = True try: import weechat as w except ImportError: print("This script must be run under WeeChat.") import_ok = False def close_cb(*kwargs): return w.WEECHAT_RC_OK def command_main(data, buffer, args): infolist = w.infolist_get("buffer", "", "") buffer_groups = {} results = [] buffer_count = 0 merge_count = 0 numbers = set() while w.infolist_next(infolist): bplugin = w.infolist_string(infolist, "plugin_name") bname = w.infolist_string(infolist, "name") bpointer = w.infolist_pointer(infolist, "pointer") bnumber = w.infolist_integer(infolist, "number") btype = w.buffer_get_string(bpointer, 'localvar_type') if not bnumber in numbers: numbers.add(bnumber) else: merge_count += 1 if btype == 'server': bdesc = 'servers' elif btype == 'channel': bdesc = 'channels' elif btype == 'private': bdesc = 'queries' else: bdesc = bplugin buffer_groups.setdefault(bdesc,[]).append({'name': bname, 'pointer': bpointer}) w.infolist_free(infolist) infolist = w.infolist_get("window", "", "") windows_v = set() windows_h = set() windows = set() while w.infolist_next(infolist): window = w.infolist_pointer(infolist, "pointer") window_w = w.infolist_integer(infolist, "width_pct") window_h = w.infolist_integer(infolist, "height_pct") windows.add(window) if window_h == 100 and window_w != 100: windows_v.add(window) elif window_w == 100 and window_h != 100: windows_h.add(window) #else: #both 100%, thus no splits w.infolist_free(infolist) window_count = len(windows) for desc, buffers in buffer_groups.items(): buffer_count += len(buffers) results.append('%i %s' % (len(buffers), desc)) buffer_stats = ', '.join(sorted(results, key = lambda item: (int(item.partition(' ')[0]) if item[0].isdigit() else float('inf'), item),reverse=True)) # descending numerical sort of strings stats_string = '%i buffers (%i merged): %s; %i windows' % (buffer_count, merge_count, buffer_stats, window_count) if '-split' in args: stats_string += ": %i vertically / %i horizontally split" % (len(windows_v), len(windows_h)) w.command("", "/input insert %s" % stats_string) return w.WEECHAT_RC_OK if __name__ == "__main__" and import_ok: if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, SCRIPT_CLOSE_CB, ""): w.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, "-split", "Inserts useless statistics about your open buffers and windows into your input line.\n" "-split: Include information about window splits.", "-split", "command_main", "") weechat-scripts/python/urlbuf.py0000644000175100017510000001574315112622401016045 0ustar manumanu# -*- coding: utf-8 -*- # Copyright (c) 2011-2014 by Jani Kesänen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # A common buffer for URLs # # Collects received URLs from public and private messages into a single # buffer. This buffer is especially handy if you spend lot's of time afk # and you don't want to miss any of the cat pictures/videos that were pasted # while you were doing something meaningful. # # This script has been originally developed for WeeChat version 0.3.5. May # not work properly (or at all) on older versions. # # History: # 2019-07-07, nils_2@freenode.#weechat # version 0.4: - fix bug when script unloads. # - add search for buffer name and display buffer name # 2019-07-07, nils_2@freenode.#weechat # version 0.3: - make script compatible with Python 3. # 2014-09-17, Jani Kesänen # version 0.2: - added descriptions to settings. # 2011-06-07, Jani Kesänen # version 0.1: - initial release. # from __future__ import print_function SCRIPT_NAME = "urlbuf" SCRIPT_AUTHOR = "Jani Kesänen " SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "A common buffer for received URLs." import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") import_ok = False import re octet = r'(?:2(?:[0-4]\d|5[0-5])|1\d\d|\d{1,2})' ipAddr = r'%s(?:\.%s){3}' % (octet, octet) # Base domain regex off RFC 1034 and 1738 label = r'[0-9a-z][-0-9a-z]*[0-9a-z]?' domain = r'%s(?:\.%s)*\.[a-z][-0-9a-z]*[a-z]?' % (label, label) urlRe = re.compile(r'(\w+://(?:%s|%s)(?::\d+)?(?:/[^\])>\s]*)?)' % (domain, ipAddr), re.I) urlbuf_buffer = None urlbuf_settings = { "display_active_buffer" : ("on", "display URLs from the active buffer"), "display_private" : ("on", "display URLs from private messages"), "display_buffer_number" : ("on", "display the buffer's number or name (on/name/off)"), "display_nick" : ("off", "display the nick of the user"), "skip_duplicates" : ("on", "skip the URL that is already in the urlbuf"), "skip_buffers" : ("", "a comma separated list of buffer numbers or buffer names to skip"), } def is_url_listed(buffer, url): """ Search for the URL from the buffer lines. """ infolist = weechat.infolist_get("buffer_lines", buffer, "") found = False while weechat.infolist_next(infolist): message = weechat.infolist_string(infolist, "message").split(' ')[-1] if message == url: found = True break weechat.infolist_free(infolist) return found def urlbuf_print_cb(data, buffer, date, tags, displayed, highlight, prefix, message): """ Called when a message is printed. """ global urlbuf_buffer, urlbuf_tags # Exit immediately if the buffer does not exist if not urlbuf_buffer: return weechat.WEECHAT_RC_OK # Exit if the wanted tag is not in the message tagslist = tags.split(",") if not "notify_message" in tagslist: if weechat.config_get_plugin("display_private") == "on": if not "notify_private" in tagslist: return weechat.WEECHAT_RC_OK else: return weechat.WEECHAT_RC_OK # Exit if the message came from a buffer that is on the skip list buffer_number = str(weechat.buffer_get_integer(buffer, "number")) buffer_name = str(weechat.buffer_get_string(buffer, "name")) skips = set(weechat.config_get_plugin("skip_buffers").split(",")) if buffer_number in skips: return weechat.WEECHAT_RC_OK if buffer_name in skips: return weechat.WEECHAT_RC_OK if weechat.config_get_plugin("display_active_buffer") == "off": if buffer_number == weechat.buffer_get_integer(weechat.current_buffer(), "number"): return weechat.WEECHAT_RC_OK # Process all URLs from the message for url in urlRe.findall(message): output = "" if weechat.config_get_plugin("skip_duplicates") == "on": if is_url_listed(urlbuf_buffer, url): continue if weechat.config_get_plugin("display_buffer_number") == "on": output += "%s%-2d " % (weechat.color("reset"), weechat.buffer_get_integer(buffer, "number")) elif weechat.config_get_plugin("display_buffer_number") == "name": output += "%s%s " % (weechat.color("reset"), weechat.buffer_get_string(buffer, "name")) if weechat.config_get_plugin("display_nick") == "on": output += "%s " % (prefix) # Output the formatted URL into the buffer weechat.prnt(urlbuf_buffer, output + url) return weechat.WEECHAT_RC_OK def urlbuf_input_cb(data, buffer, input_data): """ A Dummy callback for buffer input. """ return weechat.WEECHAT_RC_OK def urlbuf_close_cb(data, buffer): """ A callback for buffer closing. """ global urlbuf_buffer urlbuf_buffer = None return weechat.WEECHAT_RC_OK def urlbuf2_close_cb(): global urlbuf_buffer urlbuf_buffer = None return weechat.WEECHAT_RC_OK if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "urlbuf2_close_cb", ""): version = weechat.info_get('version_number', '') or 0 # Set default settings for option, default_value in urlbuf_settings.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, default_value[0]) if int(version) >= 0x00030500: weechat.config_set_desc_plugin(option, default_value[1]) urlbuf_buffer = weechat.buffer_search("python", "urlbuf") if not urlbuf_buffer: # Create urlbuf. Sets notify to 0 as this buffer does not need to # be in hotlist. urlbuf_buffer = weechat.buffer_new("urlbuf", "urlbuf_input_cb", \ "", "urlbuf_close_cb", "") weechat.buffer_set(urlbuf_buffer, "title", "URL buffer") weechat.buffer_set(urlbuf_buffer, "notify", "0") weechat.buffer_set(urlbuf_buffer, "nicklist", "0") # Hook all public and private messages (some may think this is too limiting) weechat.hook_print("", "notify_message", "", 1, "urlbuf_print_cb", "") weechat.hook_print("", "notify_private", "", 1, "urlbuf_print_cb", "") weechat-scripts/python/slack.py0000644000175100017510000101227715112622405015647 0ustar manumanu# Copyright (c) 2014-2016 Ryan Huber # Copyright (c) 2015-2018 Tollef Fog Heen # Copyright (c) 2015-2023 Trygve Aaberge # Released under the MIT license. from __future__ import print_function, unicode_literals from collections import OrderedDict, namedtuple from datetime import date, datetime, timedelta from functools import partial, wraps from io import StringIO from itertools import chain, count, islice import copy import errno import textwrap import time import json import hashlib import os import re import sys import traceback import ssl import random import socket import string # Prevent websocket from using numpy (it's an optional dependency). We do this # because numpy causes python (and thus weechat) to crash when it's reloaded. # See https://github.com/numpy/numpy/issues/11925 sys.modules["numpy"] = None from websocket import ( # noqa: E402 ABNF, create_connection, WebSocketConnectionClosedException, ) try: basestring # Python 2 unicode str = unicode except NameError: # Python 3 basestring = unicode = str try: from collections.abc import ( ItemsView, Iterable, KeysView, Mapping, Reversible, ValuesView, ) except ImportError: from collections import ItemsView, Iterable, KeysView, Mapping, ValuesView Reversible = object try: from urllib.parse import quote, unquote, urlencode except ImportError: from urllib import quote, unquote, urlencode try: JSONDecodeError = json.JSONDecodeError except AttributeError: JSONDecodeError = ValueError # hack to make tests possible.. better way? try: import weechat except ImportError: pass SCRIPT_NAME = "slack" SCRIPT_AUTHOR = "Trygve Aaberge " SCRIPT_VERSION = "2.11.0" SCRIPT_LICENSE = "MIT" SCRIPT_DESC = "Extends WeeChat for typing notification/search/etc on slack.com" REPO_URL = "https://github.com/wee-slack/wee-slack" TYPING_DURATION = 6 RECORD_DIR = "/tmp/weeslack-debug" SLACK_API_TRANSLATOR = { "channel": { "history": "conversations.history", "join": "conversations.join", "leave": "conversations.leave", "mark": "conversations.mark", "info": "conversations.info", }, "im": { "history": "conversations.history", "join": "conversations.open", "leave": "conversations.close", "mark": "conversations.mark", "info": "conversations.info", }, "mpim": { "history": "conversations.history", "join": "conversations.open", "leave": "conversations.close", "mark": "conversations.mark", "info": "conversations.info", }, "group": { "history": "conversations.history", "join": "conversations.join", "leave": "conversations.leave", "mark": "conversations.mark", "info": "conversations.info", }, "private": { "history": "conversations.history", "join": "conversations.join", "leave": "conversations.leave", "mark": "conversations.mark", "info": "conversations.info", }, "shared": { "history": "conversations.history", "join": "conversations.join", "leave": "conversations.leave", "mark": "conversations.mark", "info": "conversations.info", }, "thread": { "history": None, "join": None, "leave": None, "mark": "subscriptions.thread.mark", }, } CONFIG_PREFIX = "plugins.var.python." + SCRIPT_NAME ###### Decorators have to be up here def slack_buffer_or_ignore(f): """ Only run this function if we're in a slack buffer, else ignore """ @wraps(f) def wrapper(data, current_buffer, *args, **kwargs): if current_buffer not in EVENTROUTER.weechat_controller.buffers: return w.WEECHAT_RC_OK return f(data, current_buffer, *args, **kwargs) return wrapper def slack_buffer_required(f): """ Only run this function if we're in a slack buffer, else print error """ @wraps(f) def wrapper(data, current_buffer, *args, **kwargs): if current_buffer not in EVENTROUTER.weechat_controller.buffers: command_name = f.__name__.replace("command_", "", 1) w.prnt( "", 'slack: command "{}" must be executed on slack buffer'.format( command_name ), ) return w.WEECHAT_RC_ERROR return f(data, current_buffer, *args, **kwargs) return wrapper def utf8_decode(f): """ Decode all arguments from byte strings to unicode strings. Use this for functions called from outside of this script, e.g. callbacks from WeeChat. """ @wraps(f) def wrapper(*args, **kwargs): return f(*decode_from_utf8(args), **decode_from_utf8(kwargs)) return wrapper NICK_GROUP_HERE = "0|Here" NICK_GROUP_AWAY = "1|Away" NICK_GROUP_EXTERNAL = "2|External" sslopt_ca_certs = {} if hasattr(ssl, "get_default_verify_paths") and callable(ssl.get_default_verify_paths): ssl_defaults = ssl.get_default_verify_paths() if ssl_defaults.cafile is not None: sslopt_ca_certs = {"ca_certs": ssl_defaults.cafile} EMOJI = {} EMOJI_WITH_SKIN_TONES_REVERSE = {} ###### Unicode handling def encode_to_utf8(data): if sys.version_info.major > 2: return data elif isinstance(data, unicode): return data.encode("utf-8") if isinstance(data, bytes): return data elif isinstance(data, Mapping): return type(data)(map(encode_to_utf8, data.items())) elif isinstance(data, Iterable): return type(data)(map(encode_to_utf8, data)) else: return data def decode_from_utf8(data): if sys.version_info.major > 2: return data elif isinstance(data, bytes): return data.decode("utf-8") if isinstance(data, unicode): return data elif isinstance(data, Mapping): return type(data)(map(decode_from_utf8, data.items())) elif isinstance(data, Iterable): return type(data)(map(decode_from_utf8, data)) else: return data class WeechatWrapper(object): def __init__(self, wrapped_class): self.wrapped_class = wrapped_class # Helper method used to encode/decode method calls. def wrap_for_utf8(self, method): def hooked(*args, **kwargs): result = method(*encode_to_utf8(args), **encode_to_utf8(kwargs)) # Prevent wrapped_class from becoming unwrapped if result == self.wrapped_class: return self return decode_from_utf8(result) return hooked # Encode and decode everything sent to/received from weechat. We use the # unicode type internally in wee-slack, but has to send utf8 to weechat. def __getattr__(self, attr): orig_attr = self.wrapped_class.__getattribute__(attr) if callable(orig_attr): return self.wrap_for_utf8(orig_attr) else: return decode_from_utf8(orig_attr) # Ensure all lines sent to weechat specifies a prefix. For lines after the # first, we want to disable the prefix, which we do by specifying the same # number of spaces, so it aligns correctly. def prnt_date_tags(self, buffer, date, tags, message): if weechat_version < 0x04000000: prefix, _, _ = message.partition("\t") prefix = weechat.string_remove_color(encode_to_utf8(prefix), "") prefix_spaces = " " * weechat.strlen_screen(prefix) message = message.replace("\n", "\n{}\t".format(prefix_spaces)) return self.wrap_for_utf8(self.wrapped_class.prnt_date_tags)( buffer, date, tags, message ) class ProxyWrapper(object): def __init__(self): self.proxy_name = w.config_string(w.config_get("weechat.network.proxy_curl")) self.proxy_string = "" self.proxy_type = "" self.proxy_address = "" self.proxy_port = "" self.proxy_user = "" self.proxy_password = "" self.has_proxy = False if self.proxy_name: self.proxy_string = "weechat.proxy.{}".format(self.proxy_name) self.proxy_type = w.config_string( w.config_get("{}.type".format(self.proxy_string)) ) if self.proxy_type == "http": self.proxy_address = w.config_string( w.config_get("{}.address".format(self.proxy_string)) ) self.proxy_port = w.config_integer( w.config_get("{}.port".format(self.proxy_string)) ) self.proxy_user = w.config_string( w.config_get("{}.username".format(self.proxy_string)) ) self.proxy_password = w.config_string( w.config_get("{}.password".format(self.proxy_string)) ) self.has_proxy = True else: w.prnt( "", "\nWarning: weechat.network.proxy_curl is set to {} type (name : {}, conf string : {}). Only HTTP proxy is supported.\n\n".format( self.proxy_type, self.proxy_name, self.proxy_string ), ) def curl(self): if not self.has_proxy: return "" if self.proxy_user and self.proxy_password: user = "{}:{}@".format(self.proxy_user, self.proxy_password) else: user = "" if self.proxy_port: port = ":{}".format(self.proxy_port) else: port = "" return "-x{}{}{}".format(user, self.proxy_address, port) class MappingReversible(Mapping, Reversible): def keys(self): return KeysViewReversible(self) def items(self): return ItemsViewReversible(self) def values(self): return ValuesViewReversible(self) class KeysViewReversible(KeysView, Reversible): def __reversed__(self): return reversed(self._mapping) class ItemsViewReversible(ItemsView, Reversible): def __reversed__(self): for key in reversed(self._mapping): yield (key, self._mapping[key]) class ValuesViewReversible(ValuesView, Reversible): def __reversed__(self): for key in reversed(self._mapping): yield self._mapping[key] ##### Helpers def colorize_string(color, string, reset_color="reset"): if color: return w.color(color) + string + w.color(reset_color) else: return string def print_error(message, buffer="", warning=False): prefix = "Warning" if warning else "Error" w.prnt(buffer, "{}{}: {}".format(w.prefix("error"), prefix, message)) def print_message_not_found_error(msg_id): if msg_id: print_error( "Invalid id given, must be an existing id or a number greater " + "than 0 and less than the number of messages in the channel" ) else: print_error("No messages found in channel") def token_for_print(token): return "{}...{}".format(token[:15], token[-10:]) def format_exc_tb(): return decode_from_utf8(traceback.format_exc()) def format_exc_only(): etype, value, _ = sys.exc_info() return "".join(decode_from_utf8(traceback.format_exception_only(etype, value))) def url_encode_if_not_encoded(value): decoded = unquote(value) is_encoded = value != decoded if is_encoded: return value else: return quote(value) def get_localvar_type(slack_type): if slack_type in ("im", "mpim"): return "private" else: return "channel" def get_nick_color(nick): return w.info_get("nick_color_name", nick) def get_thread_color(thread_id): if config.color_thread_suffix == "multiple": return get_nick_color(thread_id) else: return config.color_thread_suffix def sha1_hex(s): return str(hashlib.sha1(s.encode("utf-8")).hexdigest()) def get_functions_with_prefix(prefix): return { name[len(prefix) :]: ref for name, ref in globals().items() if name.startswith(prefix) } def handle_socket_error(exception, team, caller_name): if not ( isinstance(exception, WebSocketConnectionClosedException) or exception.errno in (errno.EPIPE, errno.ECONNRESET, errno.ETIMEDOUT) ): raise w.prnt( team.channel_buffer, "Lost connection to slack team {} (on {}), reconnecting.".format( team.domain, caller_name ), ) dbg( "Socket failed on {} with exception:\n{}".format(caller_name, format_exc_tb()), level=5, ) team.set_disconnected() MESSAGE_ID_REGEX_STRING = r"(?P\d+|\$[0-9a-fA-F]{3,})" REACTION_PREFIX_REGEX_STRING = r"{}?(?P\+|-)".format( MESSAGE_ID_REGEX_STRING ) EMOJI_CHAR_REGEX_STRING = "(?P[\U00000080-\U0010ffff]+)" EMOJI_NAME_REGEX_STRING = ":(?P[a-z0-9_+-]+):" EMOJI_CHAR_OR_NAME_REGEX_STRING = "({}|{})".format( EMOJI_CHAR_REGEX_STRING, EMOJI_NAME_REGEX_STRING ) EMOJI_NAME_REGEX = re.compile(EMOJI_NAME_REGEX_STRING) EMOJI_CHAR_OR_NAME_REGEX = re.compile(EMOJI_CHAR_OR_NAME_REGEX_STRING) def regex_match_to_emoji(match, include_name=False): emoji = match.group(1) full_match = match.group() char = EMOJI.get(emoji, full_match) if include_name and char != full_match: return "{} ({})".format(char, full_match) return char def replace_string_with_emoji(text): if config.render_emoji_as_string == "both": return EMOJI_NAME_REGEX.sub( partial(regex_match_to_emoji, include_name=True), text, ) elif config.render_emoji_as_string: return text return EMOJI_NAME_REGEX.sub(regex_match_to_emoji, text) def replace_emoji_with_string(text): emoji = None key = text while emoji is None and len(key): emoji = EMOJI_WITH_SKIN_TONES_REVERSE.get(key) key = key[:-1] return emoji or text ###### New central Event router class EventRouter(object): def __init__(self): """ complete Eventrouter is the central hub we use to route: 1) incoming websocket data 2) outgoing http requests and incoming replies 3) local requests It has a recorder that, when enabled, logs most events to the location specified in RECORD_DIR. """ self.queue = [] self.slow_queue = [] self.slow_queue_timer = 0 self.teams = {} self.subteams = {} self.context = {} self.weechat_controller = WeechatController(self) self.previous_buffer = "" self.reply_buffer = {} self.cmds = get_functions_with_prefix("command_") self.proc = get_functions_with_prefix("process_") self.handlers = get_functions_with_prefix("handle_") self.local_proc = get_functions_with_prefix("local_process_") self.shutting_down = False self.recording = False self.recording_path = "/tmp" self.handle_next_hook = None self.handle_next_hook_interval = -1 def record(self): """ complete Toggles the event recorder and creates a directory for data if enabled. """ self.recording = not self.recording if self.recording: if not os.path.exists(RECORD_DIR): os.makedirs(RECORD_DIR) def record_event(self, message_json, team, file_name_field, subdir=None): """ complete Called each time you want to record an event. message_json is a json in dict form file_name_field is the json key whose value you want to be part of the file name """ now = time.time() if team: team_subdomain = team.subdomain else: team_json = message_json.get("team") if team_json: team_subdomain = team_json.get("domain") else: team_subdomain = "unknown_team" directory = "{}/{}".format(RECORD_DIR, team_subdomain) if subdir: directory = "{}/{}".format(directory, subdir) if not os.path.exists(directory): os.makedirs(directory) mtype = message_json.get(file_name_field, "unknown") f = open("{}/{}-{}.json".format(directory, now, mtype), "w") f.write("{}".format(json.dumps(message_json))) f.close() def store_context(self, data): """ A place to store data and vars needed by callback returns. We need this because WeeChat's "callback_data" has a limited size and WeeChat will crash if you exceed this size. """ identifier = "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(40) ) self.context[identifier] = data dbg("stored context {} {} ".format(identifier, data.url)) return identifier def retrieve_context(self, identifier): """ A place to retrieve data and vars needed by callback returns. We need this because WeeChat's "callback_data" has a limited size and WeeChat will crash if you exceed this size. """ return self.context.get(identifier) def delete_context(self, identifier): """ Requests can span multiple requests, so we may need to delete this as a last step """ if identifier in self.context: del self.context[identifier] def shutdown(self): """ complete This toggles shutdown mode. Shutdown mode tells us not to talk to Slack anymore. Without this, typing /quit will trigger a race with the buffer close callback and may result in you leaving every slack channel. """ self.shutting_down = not self.shutting_down def register_team(self, team): """ complete Adds a team to the list of known teams for this EventRouter. """ if isinstance(team, SlackTeam): self.teams[team.get_team_hash()] = team else: raise InvalidType(type(team)) def reconnect_if_disconnected(self): for team in self.teams.values(): time_since_last_ping = time.time() - team.last_ping_time time_since_last_pong = time.time() - team.last_pong_time if ( team.connected and time_since_last_ping < 5 and time_since_last_pong > 30 ): w.prnt( team.channel_buffer, "Lost connection to slack team {} (no pong), reconnecting.".format( team.domain ), ) team.set_disconnected() if not team.connected: team.connect() dbg("reconnecting {}".format(team)) @utf8_decode def receive_ws_callback(self, team_hash, fd): """ This is called by the global method of the same name. It is triggered when we have incoming data on a websocket, which needs to be read. Once it is read, we will ensure the data is valid JSON, add metadata, and place it back on the queue for processing as JSON. """ team = self.teams[team_hash] while True: try: # Read the data from the websocket associated with this team. opcode, data = team.ws.recv_data(control_frame=True) except ssl.SSLWantReadError: # No more data to read at this time. return w.WEECHAT_RC_OK except (WebSocketConnectionClosedException, socket.error) as e: handle_socket_error(e, team, "receive") return w.WEECHAT_RC_OK if opcode == ABNF.OPCODE_PONG: team.last_pong_time = time.time() return w.WEECHAT_RC_OK elif opcode != ABNF.OPCODE_TEXT: return w.WEECHAT_RC_OK message_json = json.loads(data.decode("utf-8")) if self.recording: self.record_event(message_json, team, "type", "websocket") message_json["wee_slack_metadata_team"] = team self.receive(message_json) def http_check_ratelimited(self, request_metadata, response): parts = response.split("\r\n\r\nHTTP/") last_header_part, body = parts[-1].split("\r\n\r\n", 1) header_lines = last_header_part.split("\r\n") http_status = header_lines[0].split(" ")[1] if http_status == "429": for header in header_lines[1:]: name, value = header.split(":", 1) if name.lower() == "retry-after": retry_after = int(value.strip()) request_metadata.retry_time = time.time() + retry_after return "", "ratelimited" return body, "" def retry_request(self, request_metadata, data, return_code, err): self.reply_buffer.pop(request_metadata.response_id, None) self.delete_context(data) retry_text = ( "retrying" if request_metadata.should_try() else "will not retry after too many failed attempts" ) team = ( "for team {}".format(request_metadata.team) if request_metadata.team else "with token {}".format(token_for_print(request_metadata.token)) ) w.prnt( "", ( "Failed requesting {} {}, {}. " + "If this persists, try increasing slack_timeout. Error (code {}): {}" ).format( request_metadata.request, team, retry_text, return_code, err, ), ) dbg( "{} failed with return_code {} and error {}. stack:\n{}".format( request_metadata.request, return_code, err, "".join(traceback.format_stack()), ), level=5, ) self.receive(request_metadata) @utf8_decode def receive_httprequest_callback(self, data, command, return_code, out, err): """ complete Receives the result of an http request we previously handed off to WeeChat (WeeChat bundles libcurl). WeeChat can fragment replies, so it buffers them until the reply is complete. It is then populated with metadata here so we can identify where the request originated and route properly. """ request_metadata = self.retrieve_context(data) dbg( "RECEIVED CALLBACK with request of {} id of {} and code {} of length {}".format( request_metadata.request, request_metadata.response_id, return_code, len(out), ) ) if return_code == 0: if len(out) > 0: if request_metadata.response_id not in self.reply_buffer: self.reply_buffer[request_metadata.response_id] = StringIO() self.reply_buffer[request_metadata.response_id].write(out) response = self.reply_buffer[request_metadata.response_id].getvalue() body, error = self.http_check_ratelimited(request_metadata, response) if error: self.retry_request(request_metadata, data, return_code, error) else: j = json.loads(body) try: j[ "wee_slack_process_method" ] = request_metadata.request_normalized if self.recording: self.record_event( j, request_metadata.team, "wee_slack_process_method", "http", ) j["wee_slack_request_metadata"] = request_metadata self.reply_buffer.pop(request_metadata.response_id) self.receive(j) self.delete_context(data) except: dbg("HTTP REQUEST CALLBACK FAILED", True) # We got an empty reply and this is weird so just ditch it and retry else: dbg("length was zero, probably a bug..") self.delete_context(data) self.receive(request_metadata) elif return_code == -1: if request_metadata.response_id not in self.reply_buffer: self.reply_buffer[request_metadata.response_id] = StringIO() self.reply_buffer[request_metadata.response_id].write(out) else: self.retry_request(request_metadata, data, return_code, err) return w.WEECHAT_RC_OK def receive(self, dataobj, slow=False): """ Receives a raw object and places it on the queue for processing. Object must be known to handle_next or be JSON. """ dbg("RECEIVED FROM QUEUE") if slow: self.slow_queue.append(dataobj) else: self.queue.append(dataobj) def handle_next(self): """ complete Main handler of the EventRouter. This is called repeatedly via callback to drain events from the queue. It also attaches useful metadata and context to events as they are processed. """ wanted_interval = 100 if len(self.slow_queue) > 0 or len(self.queue) > 0: wanted_interval = 10 if ( self.handle_next_hook is None or wanted_interval != self.handle_next_hook_interval ): if self.handle_next_hook: w.unhook(self.handle_next_hook) self.handle_next_hook = w.hook_timer( wanted_interval, 0, 0, "handle_next", "" ) self.handle_next_hook_interval = wanted_interval if len(self.slow_queue) > 0 and ((self.slow_queue_timer + 1) < time.time()): dbg("from slow queue", 0) self.queue.append(self.slow_queue.pop()) self.slow_queue_timer = time.time() if len(self.queue) > 0: j = self.queue.pop(0) # Reply is a special case of a json reply from websocket. if isinstance(j, SlackRequest): if j.should_try(): if j.retry_ready(): local_process_async_slack_api_request(j, self) else: self.slow_queue.append(j) else: dbg("Max retries for Slackrequest") else: if "reply_to" in j: dbg("SET FROM REPLY") function_name = "reply" elif "type" in j: dbg("SET FROM type") function_name = j["type"] elif "wee_slack_process_method" in j: dbg("SET FROM META") function_name = j["wee_slack_process_method"] else: dbg("SET FROM NADA") function_name = "unknown" request = j.get("wee_slack_request_metadata") if request: team = request.team channel = request.channel metadata = request.metadata callback = request.callback else: team = j.get("wee_slack_metadata_team") channel = None metadata = {} callback = None if team: if "channel" in j: channel_id = ( j["channel"]["id"] if isinstance(j["channel"], dict) else j["channel"] ) channel = team.channels.get(channel_id, channel) if "user" in j: user_id = ( j["user"]["id"] if isinstance(j["user"], dict) else j["user"] ) metadata["user"] = team.users.get(user_id) dbg("running {}".format(function_name)) if callable(callback): callback(j, self, team, channel, metadata) elif ( function_name.startswith("local_") and function_name in self.local_proc ): self.local_proc[function_name](j, self, team, channel, metadata) elif function_name in self.proc: self.proc[function_name](j, self, team, channel, metadata) elif function_name in self.handlers: self.handlers[function_name](j, self, team, channel, metadata) else: dbg("Callback not implemented for event: {}".format(function_name)) def handle_next(data, remaining_calls): try: EVENTROUTER.handle_next() except: if config.debug_mode: traceback.print_exc() else: pass return w.WEECHAT_RC_OK class WeechatController(object): """ Encapsulates our interaction with WeeChat """ def __init__(self, eventrouter): self.eventrouter = eventrouter self.buffers = {} self.previous_buffer = None def iter_buffers(self): for b in self.buffers: yield (b, self.buffers[b]) def register_buffer(self, buffer_ptr, channel): """ complete Adds a WeeChat buffer to the list of handled buffers for this EventRouter """ if isinstance(buffer_ptr, basestring): self.buffers[buffer_ptr] = channel else: raise InvalidType(type(buffer_ptr)) def unregister_buffer(self, buffer_ptr, update_remote=False, close_buffer=False): """ complete Adds a WeeChat buffer to the list of handled buffers for this EventRouter """ channel = self.buffers.get(buffer_ptr) if channel: channel.destroy_buffer(update_remote) del self.buffers[buffer_ptr] if close_buffer: w.buffer_close(buffer_ptr) def get_channel_from_buffer_ptr(self, buffer_ptr): return self.buffers.get(buffer_ptr) def get_all(self, buffer_ptr): return self.buffers def get_previous_buffer_ptr(self): return self.previous_buffer def set_previous_buffer(self, data): self.previous_buffer = data ###### New Local Processors def local_process_async_slack_api_request(request, event_router): """ complete Sends an API request to Slack. You'll need to give this a well formed SlackRequest object. DEBUGGING!!! The context here cannot be very large. WeeChat will crash. """ if not event_router.shutting_down: weechat_request = "url:{}".format(request.request_string()) weechat_request += "&nonce={}".format( "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(4) ) ) request.tried() options = request.options() options["header"] = "1" context = event_router.store_context(request) w.hook_process_hashtable( weechat_request, options, config.slack_timeout, "receive_httprequest_callback", context, ) ###### New Callbacks @utf8_decode def ws_ping_cb(data, remaining_calls): for team in EVENTROUTER.teams.values(): if team.ws and team.connected: try: team.ws.ping() team.last_ping_time = time.time() except (WebSocketConnectionClosedException, socket.error) as e: handle_socket_error(e, team, "ping") return w.WEECHAT_RC_OK @utf8_decode def reconnect_callback(*args): EVENTROUTER.reconnect_if_disconnected() return w.WEECHAT_RC_OK @utf8_decode def buffer_renamed_cb(data, signal, current_buffer): channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if ( isinstance(channel, SlackChannelCommon) and not channel.buffer_rename_in_progress ): if w.buffer_get_string(channel.channel_buffer, "old_full_name"): channel.label_full_drop_prefix = True channel.label_full = w.buffer_get_string(channel.channel_buffer, "name") else: channel.label_short_drop_prefix = True channel.label_short = w.buffer_get_string( channel.channel_buffer, "short_name" ) channel.rename() return w.WEECHAT_RC_OK @utf8_decode def buffer_closing_callback(data, signal, current_buffer): """ Receives a callback from WeeChat when a buffer is being closed. """ EVENTROUTER.weechat_controller.unregister_buffer(current_buffer, True, False) return w.WEECHAT_RC_OK @utf8_decode def buffer_input_callback(signal, buffer_ptr, data): """ incomplete Handles everything a user types in the input bar. In our case this includes add/remove reactions, modifying messages, and sending messages. """ if weechat_version < 0x2090000: data = data.replace("\r", "\n") eventrouter = eval(signal) channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(buffer_ptr) if not channel: return w.WEECHAT_RC_ERROR reaction = re.match( r"{}{}\s*$".format( REACTION_PREFIX_REGEX_STRING, EMOJI_CHAR_OR_NAME_REGEX_STRING ), data, ) substitute = re.match("{}?s/".format(MESSAGE_ID_REGEX_STRING), data) if reaction: emoji = reaction.group("emoji_char") or reaction.group("emoji_name") if reaction.group("reaction_change") == "+": channel.send_add_reaction(reaction.group("msg_id"), emoji) elif reaction.group("reaction_change") == "-": channel.send_remove_reaction(reaction.group("msg_id"), emoji) elif substitute: try: old, new, flags = re.split(r"(? ">channel" and user presence via " name" <-> "+name". """ for buf in EVENTROUTER.weechat_controller.buffers.values(): buf.refresh() return w.WEECHAT_RC_OK def quit_notification_callback(data, signal, args): stop_talking_to_slack() return w.WEECHAT_RC_OK @utf8_decode def typing_notification_cb(data, signal, current_buffer): msg = w.buffer_get_string(current_buffer, "input") if len(msg) > 8 and msg[0] != "/": global typing_timer now = time.time() if typing_timer + 4 < now: channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if channel and channel.type != "thread": identifier = channel.identifier request = {"type": "typing", "channel": identifier} channel.team.send_to_websocket(request, expect_reply=False) typing_timer = now return w.WEECHAT_RC_OK @utf8_decode def typing_update_cb(data, remaining_calls): w.bar_item_update("slack_typing_notice") return w.WEECHAT_RC_OK @utf8_decode def slack_never_away_cb(data, remaining_calls): if config.never_away: for team in EVENTROUTER.teams.values(): set_own_presence_active(team) return w.WEECHAT_RC_OK @utf8_decode def typing_bar_item_cb(data, item, current_window, current_buffer, extra_info): """ Privides a bar item indicating who is typing in the current channel AND why is typing a DM to you globally. """ typers = [] current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) # first look for people typing in this channel if current_channel: # this try is mostly becuase server buffers don't implement is_someone_typing try: if current_channel.type != "im" and current_channel.is_someone_typing(): typers += current_channel.get_typing_list() except: pass # here is where we notify you that someone is typing in DM # regardless of which buffer you are in currently for team in EVENTROUTER.teams.values(): for channel in team.channels.values(): if channel.type == "im": if channel.is_someone_typing(): typers.append("D/" + channel.name) typing = ", ".join(typers) if typing != "": typing = colorize_string(config.color_typing_notice, "typing: " + typing) return typing @utf8_decode def away_bar_item_cb(data, item, current_window, current_buffer, extra_info): channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if not channel: return "" if channel.team.is_user_present(channel.team.myidentifier): return "" else: away_color = w.config_string(w.config_get("weechat.color.item_away")) if channel.team.my_manual_presence == "away": return colorize_string(away_color, "manual away") else: return colorize_string(away_color, "auto away") @utf8_decode def channel_completion_cb(data, completion_item, current_buffer, completion): """ Adds all channels on all teams to completion list """ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) should_include_channel = lambda channel: channel.active and channel.type in [ "channel", "group", "private", "shared", ] other_teams = [ team for team in EVENTROUTER.teams.values() if not current_channel or team != current_channel.team ] for team in other_teams: for channel in team.channels.values(): if should_include_channel(channel): completion_list_add( completion, channel.name, 0, w.WEECHAT_LIST_POS_SORT ) if current_channel: for channel in sorted( current_channel.team.channels.values(), key=lambda channel: channel.name, reverse=True, ): if should_include_channel(channel): completion_list_add( completion, channel.name, 0, w.WEECHAT_LIST_POS_BEGINNING ) if should_include_channel(current_channel): completion_list_add( completion, current_channel.name, 0, w.WEECHAT_LIST_POS_BEGINNING ) return w.WEECHAT_RC_OK @utf8_decode def dm_completion_cb(data, completion_item, current_buffer, completion): """ Adds all dms/mpdms on all teams to completion list """ for team in EVENTROUTER.teams.values(): for channel in team.channels.values(): if channel.active and channel.type in ["im", "mpim"]: completion_list_add( completion, channel.name, 0, w.WEECHAT_LIST_POS_SORT ) return w.WEECHAT_RC_OK @utf8_decode def nick_completion_cb(data, completion_item, current_buffer, completion): """ Adds all @-prefixed nicks to completion list """ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if current_channel is None or current_channel.members is None: return w.WEECHAT_RC_OK base_command = completion_get_string(completion, "base_command") if base_command in ["invite", "msg", "query", "whois"]: members = current_channel.team.members else: members = current_channel.members for member in members: user = current_channel.team.users.get(member) if user and not user.deleted: completion_list_add(completion, user.name, 1, w.WEECHAT_LIST_POS_SORT) completion_list_add(completion, "@" + user.name, 1, w.WEECHAT_LIST_POS_SORT) return w.WEECHAT_RC_OK @utf8_decode def emoji_completion_cb(data, completion_item, current_buffer, completion): """ Adds all :-prefixed emoji to completion list """ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if current_channel is None: return w.WEECHAT_RC_OK base_word = completion_get_string(completion, "base_word") reaction = re.match(REACTION_PREFIX_REGEX_STRING + ":", base_word) prefix = reaction.group(0) if reaction else ":" for emoji in current_channel.team.emoji_completions: completion_list_add( completion, prefix + emoji + ":", 0, w.WEECHAT_LIST_POS_SORT ) return w.WEECHAT_RC_OK @utf8_decode def thread_completion_cb(data, completion_item, current_buffer, completion): """ Adds all $-prefixed thread ids to completion list """ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if current_channel is None or not hasattr(current_channel, "hashed_messages"): return w.WEECHAT_RC_OK threads = ( x for x in current_channel.hashed_messages.items() if isinstance(x[0], str) ) for thread_id, message_ts in sorted(threads, key=lambda item: item[1]): message = current_channel.messages.get(message_ts) if message and message.number_of_replies(): completion_list_add( completion, "$" + thread_id, 0, w.WEECHAT_LIST_POS_BEGINNING ) return w.WEECHAT_RC_OK @utf8_decode def topic_completion_cb(data, completion_item, current_buffer, completion): """ Adds topic for current channel to completion list """ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if current_channel is None: return w.WEECHAT_RC_OK topic = current_channel.render_topic() channel_names = [channel.name for channel in current_channel.team.channels.values()] if topic.split(" ", 1)[0] in channel_names: topic = "{} {}".format(current_channel.name, topic) completion_list_add(completion, topic, 0, w.WEECHAT_LIST_POS_SORT) return w.WEECHAT_RC_OK @utf8_decode def usergroups_completion_cb(data, completion_item, current_buffer, completion): """ Adds all @-prefixed usergroups to completion list """ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if current_channel is None: return w.WEECHAT_RC_OK subteam_handles = [ subteam.handle for subteam in current_channel.team.subteams.values() ] for group in subteam_handles + ["@channel", "@everyone", "@here"]: completion_list_add(completion, group, 1, w.WEECHAT_LIST_POS_SORT) return w.WEECHAT_RC_OK @utf8_decode def complete_next_cb(data, current_buffer, command): """Extract current word, if it is equal to a nick, prefix it with @ and rely on nick_completion_cb adding the @-prefixed versions to the completion lists, then let WeeChat's internal completion do its thing """ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) if ( not hasattr(current_channel, "members") or current_channel is None or current_channel.members is None ): return w.WEECHAT_RC_OK line_input = w.buffer_get_string(current_buffer, "input") current_pos = w.buffer_get_integer(current_buffer, "input_pos") - 1 input_length = w.buffer_get_integer(current_buffer, "input_length") word_start = 0 word_end = input_length # If we're on a non-word, look left for something to complete while ( current_pos >= 0 and line_input[current_pos] != "@" and not line_input[current_pos].isalnum() ): current_pos = current_pos - 1 if current_pos < 0: current_pos = 0 for l in range(current_pos, 0, -1): if line_input[l] != "@" and not line_input[l].isalnum(): word_start = l + 1 break for l in range(current_pos, input_length): if not line_input[l].isalnum(): word_end = l break word = line_input[word_start:word_end] for member in current_channel.members: user = current_channel.team.users.get(member) if user and user.name == word: # Here, we cheat. Insert a @ in front and rely in the @ # nicks being in the completion list w.buffer_set( current_buffer, "input", line_input[:word_start] + "@" + line_input[word_start:], ) w.buffer_set( current_buffer, "input_pos", str(w.buffer_get_integer(current_buffer, "input_pos") + 1), ) return w.WEECHAT_RC_OK_EAT return w.WEECHAT_RC_OK def script_unloaded(): stop_talking_to_slack() return w.WEECHAT_RC_OK def stop_talking_to_slack(): """ complete Prevents a race condition where quitting closes buffers which triggers leaving the channel because of how close buffer is handled """ if "EVENTROUTER" in globals(): EVENTROUTER.shutdown() for team in EVENTROUTER.teams.values(): team.ws.shutdown() return w.WEECHAT_RC_OK ##### New Classes class SlackRequest(object): """ Encapsulates a Slack api request. Valuable as an object that we can add to the queue and/or retry. makes a SHA of the requst url and current time so we can re-tag this on the way back through. """ def __init__( self, team, request, post_data=None, channel=None, metadata=None, retries=3, token=None, cookies=None, callback=None, ): if team is None and token is None: raise ValueError("Both team and token can't be None") self.team = team self.request = request self.post_data = post_data if post_data else {} self.channel = channel self.metadata = metadata if metadata else {} self.retries = retries self.retry_time = 0 self.token = token if token else team.token self.cookies = cookies or {} if ":" in self.token: token, cookie = self.token.split(":", 1) self.token = token if cookie.startswith("d="): for name, value in [c.split("=") for c in cookie.split(";")]: self.cookies[name] = value else: self.cookies["d"] = cookie self.callback = callback self.domain = "api.slack.com" self.reset() def reset(self): self.tries = 0 self.start_time = time.time() self.request_normalized = re.sub(r"\W+", "", self.request) self.url = "https://{}/api/{}?{}".format( self.domain, self.request, urlencode(encode_to_utf8(self.post_data)) ) self.response_id = sha1_hex("{}{}".format(self.url, self.start_time)) def __repr__(self): return ( "SlackRequest(team={}, request='{}', post_data={}, retries={}, token='{}', " "cookies={}, tries={}, start_time={})" ).format( self.team, self.request, self.post_data, self.retries, token_for_print(self.token), self.cookies, self.tries, self.start_time, ) def request_string(self): return "{}".format(self.url) def options(self): cookies = "; ".join( [ "{}={}".format(key, url_encode_if_not_encoded(value)) for key, value in self.cookies.items() ] ) return { "useragent": "wee_slack {}".format(SCRIPT_VERSION), "httpheader": "Authorization: Bearer {}".format(self.token), "cookie": cookies, } def options_as_cli_args(self): options = self.options() options["user-agent"] = options.pop("useragent") httpheader = options.pop("httpheader") headers = [": ".join(x) for x in options.items()] + httpheader.split("\n") return ["-H{}".format(header) for header in headers] def tried(self): self.tries += 1 self.response_id = sha1_hex("{}{}".format(self.url, time.time())) def should_try(self): return self.tries < self.retries def retry_ready(self): if self.retry_time: return time.time() > self.retry_time else: return (self.start_time + (self.tries**2)) < time.time() class SlackSubteam(object): """ Represents a slack group or subteam """ def __init__(self, originating_team_id, is_member, **kwargs): self.handle = "@{}".format(kwargs["handle"]) self.identifier = kwargs["id"] self.name = kwargs["name"] self.description = kwargs.get("description") self.team_id = originating_team_id self.is_member = is_member def __repr__(self): return "Name:{} Identifier:{}".format(self.name, self.identifier) def __eq__(self, compare_str): return compare_str == self.identifier class SlackTeam(object): """ incomplete Team object under which users and channels live.. Does lots. """ def __init__( self, eventrouter, token, team_hash, websocket_url, team_info, subteams, nick, myidentifier, my_manual_presence, users, bots, channels, **kwargs ): self.slack_api_translator = copy.deepcopy(SLACK_API_TRANSLATOR) self.identifier = team_info["id"] self.type = "team" self.active = True self.team_hash = team_hash self.ws_url = websocket_url self.connected = False self.connecting_rtm = False self.connecting_ws = False self.ws = None self.ws_counter = 0 self.ws_replies = {} self.last_ping_time = 0 self.last_pong_time = time.time() self.eventrouter = eventrouter self.token = token self.team = self self.subteams = subteams self.team_info = team_info self.subdomain = team_info["domain"] self.domain = self.subdomain + ".slack.com" self.set_name() self.nick = nick self.myidentifier = myidentifier self.my_manual_presence = my_manual_presence try: if self.channels: for c in channels.keys(): if not self.channels.get(c): self.channels[c] = channels[c] except: self.channels = channels self.users = users self.bots = bots self.channel_buffer = None self.got_history = True self.history_needs_update = False self.create_buffer() self.set_muted_channels(kwargs.get("muted_channels", "")) self.set_highlight_words(kwargs.get("highlight_words", "")) for c in self.channels.keys(): channels[c].set_related_server(self) channels[c].check_should_open() # Last step is to make sure my nickname is the set color self.users[self.myidentifier].force_color( w.config_string(w.config_get("weechat.color.chat_nick_self")) ) # This highlight step must happen after we have set related server self.load_emoji_completions() def __repr__(self): return "domain={} nick={}".format(self.subdomain, self.nick) def __eq__(self, compare_str): return ( compare_str == self.token or compare_str == self.domain or compare_str == self.subdomain ) @property def members(self): return self.users.keys() def load_emoji_completions(self): self.emoji_completions = list(EMOJI.keys()) if self.emoji_completions: s = SlackRequest(self, "emoji.list") self.eventrouter.receive(s) def add_channel(self, channel): self.channels[channel["id"]] = channel channel.set_related_server(self) def generate_usergroup_map(self): return {s.handle: s.identifier for s in self.subteams.values()} def set_name(self): alias = config.server_aliases.get(self.subdomain) if alias: self.name = alias elif config.short_buffer_names: self.name = self.subdomain else: self.name = "slack.{}".format(self.subdomain) def create_buffer(self): if not self.channel_buffer: self.channel_buffer = w.buffer_new( self.name, "buffer_input_callback", "EVENTROUTER", "", "" ) self.eventrouter.weechat_controller.register_buffer( self.channel_buffer, self ) w.buffer_set(self.channel_buffer, "input_prompt", self.nick) w.buffer_set(self.channel_buffer, "input_multiline", "1") w.buffer_set(self.channel_buffer, "localvar_set_type", "server") w.buffer_set(self.channel_buffer, "localvar_set_slack_type", self.type) w.buffer_set(self.channel_buffer, "localvar_set_nick", self.nick) w.buffer_set(self.channel_buffer, "localvar_set_server", self.name) w.buffer_set( self.channel_buffer, "localvar_set_completion_default_template", "${weechat.completion.default_template}|%(usergroups)|%(emoji)", ) self.buffer_merge() def buffer_merge(self, config_value=None): if not config_value: config_value = w.config_string(w.config_get("irc.look.server_buffer")) if config_value == "merge_with_core": w.buffer_merge(self.channel_buffer, w.buffer_search_main()) else: w.buffer_unmerge(self.channel_buffer, 0) def destroy_buffer(self, update_remote): pass def set_muted_channels(self, muted_str): self.muted_channels = {x for x in muted_str.split(",") if x} for channel in self.channels.values(): channel.set_highlights() channel.rename() def set_highlight_words(self, highlight_str): self.highlight_words = {x for x in highlight_str.split(",") if x} for channel in self.channels.values(): channel.set_highlights() def formatted_name(self): return self.domain def buffer_prnt(self, data, message=False): tag_name = "team_message" if message else "team_info" ts = SlackTS() w.prnt_date_tags(self.channel_buffer, ts.major, tag(ts, tag_name), data) def send_message(self, message, subtype=None, request_dict_ext={}): w.prnt("", "ERROR: Sending a message in the team buffer is not supported") def find_channel_by_members(self, members, channel_type=None): for channel in self.channels.values(): if channel.members == members and ( channel_type is None or channel.type == channel_type ): return channel def get_channel_map(self): return {v.name: k for k, v in self.channels.items()} def get_username_map(self): return {v.name: k for k, v in self.users.items()} def get_team_hash(self): return self.team_hash @staticmethod def generate_team_hash(team_id, subdomain): return str(sha1_hex("{}{}".format(team_id, subdomain))) def refresh(self): pass def is_user_present(self, user_id): user = self.users.get(user_id) if user and user.presence == "active": return True else: return False def mark_read(self, ts=None, update_remote=True, force=False): pass def connect(self): if not self.connected and not self.connecting_ws: if self.ws_url: self.connecting_ws = True try: # only http proxy is currently supported proxy = ProxyWrapper() timeout = config.slack_timeout / 1000 cookie = SlackRequest(self.team, "").options()["cookie"] if proxy.has_proxy: ws = create_connection( self.ws_url, cookie=cookie, timeout=timeout, sslopt=sslopt_ca_certs, http_proxy_host=proxy.proxy_address, http_proxy_port=proxy.proxy_port, http_proxy_auth=(proxy.proxy_user, proxy.proxy_password), ) else: ws = create_connection( self.ws_url, cookie=cookie, timeout=timeout, sslopt=sslopt_ca_certs, ) self.hook = w.hook_fd( ws.sock.fileno(), 1, 0, 0, "receive_ws_callback", self.get_team_hash(), ) ws.sock.setblocking(0) except: w.prnt( self.channel_buffer, "Failed connecting to slack team {}, retrying.".format( self.domain ), ) dbg( "connect failed with exception:\n{}".format(format_exc_tb()), level=5, ) return False finally: self.connecting_ws = False self.ws = ws self.set_reconnect_url(None) self.set_connected() elif not self.connecting_rtm: # The fast reconnect failed, so start over-ish for chan in self.channels: self.channels[chan].history_needs_update = True s = get_rtm_connect_request(self.token, retries=999, team=self) self.eventrouter.receive(s) self.connecting_rtm = True def set_connected(self): self.connected = True self.last_pong_time = time.time() self.buffer_prnt( "Connected to Slack team {} ({}) with username {}".format( self.team_info["name"], self.domain, self.nick ) ) dbg("connected to {}".format(self.domain)) if config.background_load_all_history: for channel in self.channels.values(): if channel.channel_buffer: channel.get_history(slow_queue=True) else: current_channel = self.eventrouter.weechat_controller.buffers.get( w.current_buffer() ) if ( isinstance(current_channel, SlackChannelCommon) and current_channel.team == self ): current_channel.get_history(slow_queue=True) def set_disconnected(self): w.unhook(self.hook) self.connected = False def set_reconnect_url(self, url): self.ws_url = url def next_ws_transaction_id(self): self.ws_counter += 1 return self.ws_counter def send_to_websocket(self, data, expect_reply=True): data["id"] = self.next_ws_transaction_id() message = json.dumps(data) try: if expect_reply: self.ws_replies[data["id"]] = data self.ws.send(encode_to_utf8(message)) dbg("Sent {}...".format(message[:100])) except (WebSocketConnectionClosedException, socket.error) as e: handle_socket_error(e, self, "send") def update_member_presence(self, user, presence): user.presence = presence for c in self.channels: c = self.channels[c] if user.id in c.members: c.buffer_name_needs_update = True c.update_nicklist(user.id) def subscribe_users_presence(self): # FIXME: There is a limitation in the API to the size of the # json we can send. # We should try to be smarter to fetch the users whom we want to # subscribe to. users = list(self.users.keys())[:750] if self.myidentifier not in users: users.append(self.myidentifier) self.send_to_websocket( { "type": "presence_sub", "ids": users, }, expect_reply=False, ) class SlackChannelCommon(object): def __init__(self): self.label_full_drop_prefix = False self.label_full = None self.label_short_drop_prefix = False self.label_short = None self.buffer_rename_in_progress = False def prnt_message( self, message, history_message=False, no_log=False, force_render=False ): text = self.render(message, force_render) thread_channel = isinstance(self, SlackThreadChannel) if message.subtype == "join": tagset = "join" prefix = w.prefix("join").strip() elif message.subtype == "leave": tagset = "leave" prefix = w.prefix("quit").strip() elif message.subtype == "topic": tagset = "topic" prefix = w.prefix("network").strip() else: channel_type = self.parent_channel.type if thread_channel else self.type if channel_type in ["im", "mpim"]: tagset = "dm" else: tagset = "channel" if message.subtype == "me_message": prefix = w.prefix("action").rstrip() else: prefix = message.sender extra_tags = None if message.subtype == "thread_broadcast": extra_tags = [message.subtype] elif isinstance(message, SlackThreadMessage) and not thread_channel: if config.thread_messages_in_channel: extra_tags = [message.subtype] else: return self.buffer_prnt( prefix, text, message.ts, tagset=tagset, tag_nick=message.sender_plain, history_message=history_message, no_log=no_log, extra_tags=extra_tags, ) def print_getting_history(self): if self.channel_buffer: ts = SlackTS() w.buffer_set(self.channel_buffer, "print_hooks_enabled", "0") w.prnt_date_tags( self.channel_buffer, ts.major, tag(ts, backlog=True, no_log=True), "\tgetting channel history...", ) w.buffer_set(self.channel_buffer, "print_hooks_enabled", "1") def reprint_messages(self, history_message=False, no_log=True, force_render=False): if self.channel_buffer: w.buffer_clear(self.channel_buffer) self.last_line_from = None for message in self.visible_messages.values(): self.prnt_message(message, history_message, no_log, force_render) if ( self.identifier in self.pending_history_requests or config.thread_messages_in_channel and self.pending_history_requests ): self.print_getting_history() def send_message(self, message, subtype=None, request_dict_ext={}): if subtype == "me_message": message = linkify_text(message, self.team, escape_characters=False) s = SlackRequest( self.team, "chat.meMessage", {"channel": self.identifier, "text": message}, channel=self, ) self.eventrouter.receive(s) else: message = linkify_text(message, self.team) request = { "type": "message", "channel": self.identifier, "text": message, "user": self.team.myidentifier, } request.update(request_dict_ext) self.team.send_to_websocket(request) def send_add_reaction(self, msg_id, reaction): self.send_change_reaction("reactions.add", msg_id, reaction) def send_remove_reaction(self, msg_id, reaction): self.send_change_reaction("reactions.remove", msg_id, reaction) def send_change_reaction(self, method, msg_id, reaction): message = self.message_from_hash_or_index(msg_id) if message is None: print_message_not_found_error(msg_id) return reaction_name = replace_emoji_with_string(reaction) if method == "toggle": reaction = message.get_reaction(reaction_name) if reaction and self.team.myidentifier in reaction["users"]: method = "reactions.remove" else: method = "reactions.add" data = { "channel": self.identifier, "timestamp": message.ts, "name": reaction_name, } s = SlackRequest( self.team, method, data, channel=self, metadata={"reaction": reaction} ) self.eventrouter.receive(s) def edit_nth_previous_message(self, msg_id, old, new, flags): message_filter = ( lambda message: message.user_identifier == self.team.myidentifier ) message = self.message_from_hash_or_index(msg_id, message_filter) if message is None: if msg_id: print_error( "Invalid id given, must be an existing id to one of your " + "messages or a number greater than 0 and less than the number " + "of your messages in the channel" ) else: print_error("You don't have any messages in this channel") return if new == "" and old == "": post_data = {"channel": self.identifier, "ts": message.ts} s = SlackRequest(self.team, "chat.delete", post_data, channel=self) self.eventrouter.receive(s) else: num_replace = 0 if "g" in flags else 1 f = re.UNICODE f |= re.IGNORECASE if "i" in flags else 0 f |= re.MULTILINE if "m" in flags else 0 f |= re.DOTALL if "s" in flags else 0 old_message_text = message.message_json["text"] new_message_text = re.sub(old, new, old_message_text, num_replace, f) if new_message_text != old_message_text: post_data = { "channel": self.identifier, "ts": message.ts, "text": new_message_text, } s = SlackRequest(self.team, "chat.update", post_data, channel=self) self.eventrouter.receive(s) else: print_error("The regex didn't match any part of the message") def message_from_hash(self, ts_hash, message_filter=None): if not ts_hash: return ts_hash_without_prefix = ts_hash[1:] if ts_hash[0] == "$" else ts_hash ts = self.hashed_messages.get(ts_hash_without_prefix) message = self.messages.get(ts) if message is None: return if message_filter and not message_filter(message): return return message def message_from_index(self, index, message_filter=None, reverse=True): for ts in reversed(self.visible_messages) if reverse else self.visible_messages: message = self.messages[ts] if not message_filter or message_filter(message): index -= 1 if index == 0: return message def message_from_hash_or_index( self, hash_or_index=None, message_filter=None, reverse=True ): message = self.message_from_hash(hash_or_index, message_filter) if not message: if not hash_or_index: index = 1 elif hash_or_index.isdigit(): index = int(hash_or_index) else: return message = self.message_from_index(index, message_filter, reverse) return message def change_message(self, ts, message_json=None): ts = SlackTS(ts) m = self.messages.get(ts) if not m: return if message_json: m.message_json.update(message_json) if ( not isinstance(m, SlackThreadMessage) or m.subtype == "thread_broadcast" or config.thread_messages_in_channel ): new_text = self.render(m, force=True) modify_buffer_line(self.channel_buffer, ts, new_text) if isinstance(m, SlackThreadMessage) or m.thread_channel is not None: thread_channel = ( m.parent_message.thread_channel if isinstance(m, SlackThreadMessage) else m.thread_channel ) if thread_channel and thread_channel.active: new_text = thread_channel.render(m, force=True) modify_buffer_line(thread_channel.channel_buffer, ts, new_text) def mark_read(self, ts=None, update_remote=True, force=False, post_data={}): if self.new_messages or force: if self.channel_buffer: w.buffer_set(self.channel_buffer, "unread", "") w.buffer_set(self.channel_buffer, "hotlist", "-1") if not ts: ts = next(reversed(self.messages), SlackTS()) if ts > self.last_read: self.last_read = SlackTS(ts) if update_remote: args = {"channel": self.identifier, "ts": ts} args.update(post_data) mark_method = self.team.slack_api_translator[self.type].get("mark") if mark_method: s = SlackRequest(self.team, mark_method, args, channel=self) self.eventrouter.receive(s) self.new_messages = False def destroy_buffer(self, update_remote): self.channel_buffer = None self.got_history = False self.active = False class SlackChannel(SlackChannelCommon): """ Represents an individual slack channel. """ def __init__(self, eventrouter, channel_type="channel", **kwargs): super(SlackChannel, self).__init__() self.active = False for key, value in kwargs.items(): setattr(self, key, value) self.eventrouter = eventrouter self.team = kwargs.get("team") self.identifier = kwargs["id"] self.type = channel_type self.set_name(kwargs["name"]) self.slack_purpose = kwargs.get("purpose", {"value": ""}) self.topic = kwargs.get("topic", {"value": ""}) self.last_read = SlackTS(kwargs.get("last_read", 0)) self.channel_buffer = None self.got_history = False self.got_members = False self.history_needs_update = False self.pending_history_requests = set() self.messages = OrderedDict() self.visible_messages = SlackChannelVisibleMessages(self) self.hashed_messages = SlackChannelHashedMessages(self) self.thread_channels = {} self.new_messages = False self.typing = {} # short name relates to the localvar we change for typing indication self.set_members(kwargs.get("members", [])) self.unread_count_display = 0 self.last_line_from = None self.buffer_name_needs_update = False self.last_refresh_typing = False def __eq__(self, compare_str): if ( compare_str == self.slack_name or compare_str == self.formatted_name() or compare_str == self.formatted_name(style="long_default") ): return True else: return False def __repr__(self): return "Name:{} Identifier:{}".format(self.name, self.identifier) @property def muted(self): return self.identifier in self.team.muted_channels def set_name(self, slack_name): self.slack_name = slack_name self.name = self.formatted_name() self.buffer_name_needs_update = True def refresh(self): typing = self.is_someone_typing() if self.buffer_name_needs_update or typing != self.last_refresh_typing: self.last_refresh_typing = typing self.buffer_name_needs_update = False self.rename(typing) def rename(self, typing=None): if self.channel_buffer: self.buffer_rename_in_progress = True if typing is None: typing = self.is_someone_typing() present = ( self.team.is_user_present(self.user) if self.type == "im" else None ) name = self.formatted_name("long_default", typing, present) short_name = self.formatted_name("sidebar", typing, present) w.buffer_set(self.channel_buffer, "name", name) w.buffer_set(self.channel_buffer, "short_name", short_name) self.buffer_rename_in_progress = False def set_members(self, members): self.members = set(members) self.update_nicklist() def set_unread_count_display(self, count): self.unread_count_display = count self.new_messages = bool(self.unread_count_display) if self.muted and config.muted_channels_activity != "all": return for c in range(self.unread_count_display): if self.type in ["im", "mpim"]: w.buffer_set(self.channel_buffer, "hotlist", "2") else: w.buffer_set(self.channel_buffer, "hotlist", "1") def formatted_name(self, style="default", typing=False, present=None): show_typing = typing and not self.muted and config.channel_name_typing_indicator if style == "sidebar" and show_typing: prepend = ">" elif self.type == "group" or self.type == "private": prepend = config.group_name_prefix elif self.type == "shared": prepend = config.shared_name_prefix elif self.type == "im": if style != "sidebar": prepend = "" elif present and config.show_buflist_presence: prepend = "+" elif config.channel_name_typing_indicator or config.show_buflist_presence: prepend = " " else: prepend = "" elif self.type == "mpim": if style == "sidebar": prepend = "@" else: prepend = "" else: prepend = "#" name = self.label_full or self.slack_name if style == "sidebar": name = self.label_short or name if self.label_short_drop_prefix: if show_typing: name = prepend + name[1:] elif ( self.type == "im" and present and config.show_buflist_presence and name[0] == " " ): name = prepend + name[1:] else: name = prepend + name if self.muted: sidebar_color = config.color_buflist_muted_channels elif self.type == "im" and config.colorize_private_chats: sidebar_color = self.color_name else: sidebar_color = "" return colorize_string(sidebar_color, name) elif style == "long_default": if self.label_full_drop_prefix: return name else: return "{}.{}{}".format(self.team.name, prepend, name) else: if self.label_full_drop_prefix: return name else: return prepend + name def render_topic(self, fallback_to_purpose=False): topic = self.topic["value"] if not topic and fallback_to_purpose: topic = self.slack_purpose["value"] return unhtmlescape(unfurl_refs(topic)) def set_topic(self, value=None): if value is not None: self.topic = {"value": value} if self.channel_buffer: topic = self.render_topic(fallback_to_purpose=True) w.buffer_set(self.channel_buffer, "title", topic) def update_from_message_json(self, message_json): for key, value in message_json.items(): setattr(self, key, value) def open(self, update_remote=True): if update_remote: join_method = self.team.slack_api_translator[self.type].get("join") if join_method: s = SlackRequest( self.team, join_method, {"channel": self.identifier}, channel=self ) self.eventrouter.receive(s) self.create_buffer() self.active = True self.get_history() def check_should_open(self, force=False): if hasattr(self, "is_archived") and self.is_archived: return if force: self.create_buffer() return if ( getattr(self, "is_open", False) or self.unread_count_display or self.type not in ["im", "mpim"] and getattr(self, "is_member", False) ): self.create_buffer() elif self.type in ["im", "mpim"]: # If it is an IM or MPIM, we still might want to open it if there are unread messages. info_method = self.team.slack_api_translator[self.type].get("info") if info_method: s = SlackRequest( self.team, info_method, {"channel": self.identifier}, channel=self ) self.eventrouter.receive(s) def set_related_server(self, team): self.team = team def highlights(self): nick_highlights = {"@" + self.team.nick, self.team.myidentifier} subteam_highlights = { subteam.handle for subteam in self.team.subteams.values() if subteam.is_member } highlights = nick_highlights | subteam_highlights | self.team.highlight_words if self.muted and config.muted_channels_activity == "personal_highlights": return highlights else: return highlights | {"@channel", "@everyone", "@group", "@here"} def set_highlights(self): # highlight my own name and any set highlights if self.channel_buffer: h_str = ",".join(self.highlights()) w.buffer_set(self.channel_buffer, "highlight_words", h_str) if self.muted and config.muted_channels_activity != "all": notify_level = "0" if config.muted_channels_activity == "none" else "1" w.buffer_set(self.channel_buffer, "notify", notify_level) else: buffer_full_name = w.buffer_get_string(self.channel_buffer, "full_name") w.command( self.channel_buffer, "/mute /unset weechat.notify.{}".format(buffer_full_name), ) if self.muted and config.muted_channels_activity == "none": w.buffer_set( self.channel_buffer, "highlight_tags_restrict", "highlight_force" ) else: w.buffer_set(self.channel_buffer, "highlight_tags_restrict", "") for thread_channel in self.thread_channels.values(): thread_channel.set_highlights(h_str) def create_buffer(self): """ Creates the WeeChat buffer where the channel magic happens. """ if not self.channel_buffer: self.active = True self.channel_buffer = w.buffer_new( self.formatted_name(style="long_default"), "buffer_input_callback", "EVENTROUTER", "", "", ) self.eventrouter.weechat_controller.register_buffer( self.channel_buffer, self ) w.buffer_set(self.channel_buffer, "input_prompt", self.team.nick) w.buffer_set(self.channel_buffer, "input_multiline", "1") w.buffer_set( self.channel_buffer, "localvar_set_type", get_localvar_type(self.type) ) w.buffer_set(self.channel_buffer, "localvar_set_slack_type", self.type) w.buffer_set( self.channel_buffer, "localvar_set_channel", self.formatted_name() ) w.buffer_set(self.channel_buffer, "localvar_set_nick", self.team.nick) w.buffer_set( self.channel_buffer, "localvar_set_completion_default_template", "${weechat.completion.default_template}|%(usergroups)|%(emoji)", ) self.buffer_rename_in_progress = True w.buffer_set( self.channel_buffer, "short_name", self.formatted_name(style="sidebar") ) self.buffer_rename_in_progress = False self.set_highlights() self.set_topic() if self.channel_buffer: w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.name) self.update_nicklist() info_method = self.team.slack_api_translator[self.type].get("info") if info_method: s = SlackRequest( self.team, info_method, {"channel": self.identifier}, channel=self ) self.eventrouter.receive(s) if self.type == "im": join_method = self.team.slack_api_translator[self.type].get("join") if join_method: s = SlackRequest( self.team, join_method, {"users": self.user, "return_im": True}, channel=self, ) self.eventrouter.receive(s) def destroy_buffer(self, update_remote): super(SlackChannel, self).destroy_buffer(update_remote) self.messages = OrderedDict() if update_remote and not self.eventrouter.shutting_down: s = SlackRequest( self.team, self.team.slack_api_translator[self.type]["leave"], {"channel": self.identifier}, channel=self, ) self.eventrouter.receive(s) def buffer_prnt( self, nick, text, timestamp, tagset, tag_nick=None, history_message=False, no_log=False, extra_tags=None, ): data = "{}\t{}".format(format_nick(nick, self.last_line_from), text) self.last_line_from = nick ts = SlackTS(timestamp) # without this, DMs won't open automatically if not self.channel_buffer and ts > self.last_read: self.open(update_remote=False) if self.channel_buffer: # backlog messages - we will update the read marker as we print these backlog = ts <= self.last_read if not backlog: self.new_messages = True no_log = no_log or history_message and backlog self_msg = tag_nick == self.team.nick tags = tag( ts, tagset, user=tag_nick, self_msg=self_msg, backlog=backlog, no_log=no_log, extra_tags=extra_tags, ) if ( config.unhide_buffers_with_activity and not self.is_visible() and not self.muted and not no_log ): w.buffer_set(self.channel_buffer, "hidden", "0") if no_log: w.buffer_set(self.channel_buffer, "print_hooks_enabled", "0") w.prnt_date_tags(self.channel_buffer, ts.major, tags, data) if no_log: w.buffer_set(self.channel_buffer, "print_hooks_enabled", "1") if backlog or (self_msg and tagset != "join"): self.mark_read(ts, update_remote=False, force=True) def store_message(self, message_to_store): if not self.active: return old_message = self.messages.get(message_to_store.ts) if old_message and old_message.submessages and not message_to_store.submessages: message_to_store.submessages = old_message.submessages self.messages[message_to_store.ts] = message_to_store self.messages = OrderedDict(sorted(self.messages.items())) max_history = w.config_integer( w.config_get("weechat.history.max_buffer_lines_number") ) messages_to_check = islice( self.messages.items(), max(0, len(self.messages) - max_history) ) messages_to_delete = [] for ts, message in messages_to_check: if ts == message_to_store.ts: pass elif isinstance(message, SlackThreadMessage): thread_channel = self.thread_channels.get(message.thread_ts) if thread_channel is None or not thread_channel.active: messages_to_delete.append(ts) elif message.number_of_replies(): if ( message.thread_channel is None or not message.thread_channel.active ) and not any( submessage in self.messages for submessage in message.submessages ): messages_to_delete.append(ts) else: messages_to_delete.append(ts) for ts in messages_to_delete: message_hash = self.hashed_messages.get(ts) if message_hash: del self.hashed_messages[ts] del self.hashed_messages[message_hash] del self.messages[ts] def is_visible(self): return w.buffer_get_integer(self.channel_buffer, "hidden") == 0 def get_members(self): if not self.got_members: # Slack has started returning only a few members for some channels # in rtm.start. I don't know how we can check if the member list is # complete, so we have to fetch members for all channels. s = SlackRequest( self.team, "conversations.members", {"channel": self.identifier, "limit": 1000}, channel=self, ) self.eventrouter.receive(s) def get_history(self, slow_queue=False, full=False, no_log=False): if self.identifier in self.pending_history_requests: return self.print_getting_history() self.pending_history_requests.add(self.identifier) self.get_members() post_data = {"channel": self.identifier, "limit": config.history_fetch_count} if self.got_history and self.messages and not full: post_data["oldest"] = next(reversed(self.messages)) s = SlackRequest( self.team, self.team.slack_api_translator[self.type]["history"], post_data, channel=self, metadata={"slow_queue": slow_queue, "no_log": no_log}, ) self.eventrouter.receive(s, slow_queue) self.got_history = True self.history_needs_update = False def get_thread_history(self, thread_ts, slow_queue=False, no_log=False): if thread_ts in self.pending_history_requests: return if config.thread_messages_in_channel: self.print_getting_history() thread_channel = self.thread_channels.get(thread_ts) if thread_channel and thread_channel.active: thread_channel.print_getting_history() self.pending_history_requests.add(thread_ts) post_data = { "channel": self.identifier, "ts": thread_ts, "limit": config.history_fetch_count, } s = SlackRequest( self.team, "conversations.replies", post_data, channel=self, metadata={"thread_ts": thread_ts, "no_log": no_log}, ) self.eventrouter.receive(s, slow_queue) # Typing related def set_typing(self, user): if self.channel_buffer and self.is_visible(): self.typing[user.name] = time.time() self.buffer_name_needs_update = True def is_someone_typing(self): """ Walks through dict of typing folks in a channel and fast returns if any of them is actively typing. If none are, nulls the dict and returns false. """ typing_expire_time = time.time() - TYPING_DURATION for timestamp in self.typing.values(): if timestamp > typing_expire_time: return True if self.typing: self.typing = {} return False def get_typing_list(self): """ Returns the names of everyone in the channel who is currently typing. """ typing_expire_time = time.time() - TYPING_DURATION typing = [] for user, timestamp in self.typing.items(): if timestamp > typing_expire_time: typing.append(user) else: del self.typing[user] return typing def user_joined(self, user_id): # ugly hack - for some reason this gets turned into a list self.members = set(self.members) self.members.add(user_id) self.update_nicklist(user_id) def user_left(self, user_id): self.members.discard(user_id) self.update_nicklist(user_id) def update_nicklist(self, user=None): if not self.channel_buffer: return if self.type not in ["channel", "group", "mpim", "private", "shared"]: return w.buffer_set(self.channel_buffer, "nicklist", "1") # create nicklists for the current channel if they don't exist # if they do, use the existing pointer here = w.nicklist_search_group(self.channel_buffer, "", NICK_GROUP_HERE) if not here: here = w.nicklist_add_group( self.channel_buffer, "", NICK_GROUP_HERE, "weechat.color.nicklist_group", 1, ) afk = w.nicklist_search_group(self.channel_buffer, "", NICK_GROUP_AWAY) if not afk: afk = w.nicklist_add_group( self.channel_buffer, "", NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1, ) # Add External nicklist group only for shared channels if self.type == "shared": external = w.nicklist_search_group( self.channel_buffer, "", NICK_GROUP_EXTERNAL ) if not external: external = w.nicklist_add_group( self.channel_buffer, "", NICK_GROUP_EXTERNAL, "weechat.color.nicklist_group", 2, ) if user and len(self.members) < 1000: user = self.team.users.get(user) # External users that have left shared channels won't exist if not user or user.deleted: return nick = w.nicklist_search_nick(self.channel_buffer, "", user.name) # since this is a change just remove it regardless of where it is w.nicklist_remove_nick(self.channel_buffer, nick) # now add it back in to whichever.. nick_group = afk if user.is_external: nick_group = external elif self.team.is_user_present(user.identifier): nick_group = here if user.identifier in self.members: w.nicklist_add_nick( self.channel_buffer, nick_group, user.name, user.color_name, "", "", 1, ) # if we didn't get a user, build a complete list. this is expensive. else: if len(self.members) < 1000: try: for user in self.members: user = self.team.users.get(user) if user.deleted: continue nick_group = afk if user.is_external: nick_group = external elif self.team.is_user_present(user.identifier): nick_group = here w.nicklist_add_nick( self.channel_buffer, nick_group, user.name, user.color_name, "", "", 1, ) except: dbg( "DEBUG: {} {} {}".format( self.identifier, self.name, format_exc_only() ) ) else: w.nicklist_remove_all(self.channel_buffer) for fn in ["1| too", "2| many", "3| users", "4| to", "5| show"]: w.nicklist_add_group( self.channel_buffer, "", fn, w.color("white"), 1 ) def render(self, message, force=False): text = message.render(force) if isinstance(message, SlackThreadMessage): thread_hash = self.hashed_messages[message.thread_ts] if config.thread_broadcast_prefix and message.subtype == "thread_broadcast": prefix = config.thread_broadcast_prefix else: prefix = "" hash_str = colorize_string( get_thread_color(str(thread_hash)), "[{}{}]".format(prefix, thread_hash), ) return "{} {}".format(hash_str, text) return text class SlackChannelVisibleMessages(MappingReversible): """ Class with a reversible mapping interface (like a read-only OrderedDict) which doesn't include the messages older than first_ts_to_display. """ def __init__(self, channel): self.channel = channel self.first_ts_to_display = SlackTS(0) def __getitem__(self, key): if key < self.first_ts_to_display: raise KeyError(key) return self.channel.messages[key] def _is_visible(self, ts): if ts < self.first_ts_to_display: return False message = self.get(ts) if ( isinstance(message, SlackThreadMessage) and message.subtype != "thread_broadcast" and not config.thread_messages_in_channel ): return False return True def __iter__(self): for ts in self.channel.messages: if self._is_visible(ts): yield ts def __len__(self): i = 0 for _ in self: i += 1 return i def __reversed__(self): for ts in reversed(self.channel.messages): if self._is_visible(ts): yield ts class SlackChannelHashedMessages(dict): def __init__(self, channel): self.channel = channel def __missing__(self, key): if not isinstance(key, SlackTS): raise KeyError(key) hash_len = 3 full_hash = sha1_hex(str(key)) short_hash = full_hash[:hash_len] while any(x.startswith(short_hash) for x in self if isinstance(x, str)): hash_len += 1 short_hash = full_hash[:hash_len] if short_hash[:-1] in self: ts_with_same_hash = self.pop(short_hash[:-1]) other_full_hash = sha1_hex(str(ts_with_same_hash)) other_short_hash = other_full_hash[:hash_len] while short_hash == other_short_hash: hash_len += 1 short_hash = full_hash[:hash_len] other_short_hash = other_full_hash[:hash_len] self[other_short_hash] = ts_with_same_hash self[ts_with_same_hash] = other_short_hash other_message = self.channel.messages.get(ts_with_same_hash) if other_message: self.channel.change_message(other_message.ts) if other_message.thread_channel: other_message.thread_channel.rename() for thread_message in other_message.submessages: self.channel.change_message(thread_message) self[short_hash] = key self[key] = short_hash return self[key] class SlackDMChannel(SlackChannel): """ Subclass of a normal channel for person-to-person communication, which has some important differences. """ def __init__(self, eventrouter, users, myidentifier, **kwargs): dmuser = kwargs["user"] kwargs["name"] = users[dmuser].name if dmuser in users else dmuser super(SlackDMChannel, self).__init__(eventrouter, "im", **kwargs) self.update_color() self.members = {myidentifier, self.user} if dmuser in users: self.set_topic(create_user_status_string(users[dmuser].profile)) def set_related_server(self, team): super(SlackDMChannel, self).set_related_server(team) if self.user not in self.team.users: s = SlackRequest(self.team, "users.info", {"user": self.user}, channel=self) self.eventrouter.receive(s) def create_buffer(self): if not self.channel_buffer: super(SlackDMChannel, self).create_buffer() w.buffer_set(self.channel_buffer, "localvar_set_type", "private") def update_color(self): if config.colorize_private_chats: self.color_name = get_nick_color(self.name) else: self.color_name = "" def open(self, update_remote=True): self.create_buffer() self.get_history() info_method = self.team.slack_api_translator[self.type].get("info") if info_method: s = SlackRequest( self.team, info_method, {"channel": self.identifier}, channel=self ) self.eventrouter.receive(s) if update_remote: join_method = self.team.slack_api_translator[self.type].get("join") if join_method: s = SlackRequest( self.team, join_method, {"users": self.user, "return_im": True}, channel=self, ) self.eventrouter.receive(s) class SlackGroupChannel(SlackChannel): """ A group channel is a private discussion group. """ def __init__(self, eventrouter, channel_type="group", **kwargs): super(SlackGroupChannel, self).__init__(eventrouter, channel_type, **kwargs) class SlackPrivateChannel(SlackGroupChannel): """ A private channel is a private discussion group. At the time of writing, it differs from group channels in that group channels are channels initially created as private, while private channels are public channels which are later converted to private. """ def __init__(self, eventrouter, **kwargs): super(SlackPrivateChannel, self).__init__(eventrouter, "private", **kwargs) class SlackMPDMChannel(SlackChannel): """ An MPDM channel is a special instance of a 'group' channel. We change the name to look less terrible in WeeChat. """ def __init__(self, eventrouter, team_users, myidentifier, **kwargs): if kwargs.get("members"): kwargs["name"] = self.name_from_members( team_users, kwargs["members"], myidentifier ) super(SlackMPDMChannel, self).__init__(eventrouter, "mpim", **kwargs) def name_from_members(self, team_users=None, members=None, myidentifier=None): return ",".join( sorted( getattr((team_users or self.team.users).get(user_id), "name", user_id) for user_id in (members or self.members) if user_id != (myidentifier or self.team.myidentifier) ) ) def create_buffer(self): if not self.channel_buffer: self.get_members() super(SlackMPDMChannel, self).create_buffer() def open(self, update_remote=True): self.create_buffer() self.active = True self.get_history() info_method = self.team.slack_api_translator[self.type].get("info") if info_method: s = SlackRequest( self.team, info_method, {"channel": self.identifier}, channel=self ) self.eventrouter.receive(s) if update_remote: join_method = self.team.slack_api_translator[self.type].get("join") if join_method: s = SlackRequest( self.team, join_method, {"users": ",".join(self.members)}, channel=self, ) self.eventrouter.receive(s) class SlackSharedChannel(SlackChannel): def __init__(self, eventrouter, **kwargs): super(SlackSharedChannel, self).__init__(eventrouter, "shared", **kwargs) class SlackThreadChannel(SlackChannelCommon): """ A thread channel is a virtual channel. We don't inherit from SlackChannel, because most of how it operates will be different. """ def __init__(self, eventrouter, parent_channel, thread_ts): super(SlackThreadChannel, self).__init__() self.active = False self.eventrouter = eventrouter self.parent_channel = parent_channel self.thread_ts = thread_ts self.messages = SlackThreadChannelMessages(self) self.channel_buffer = None self.type = "thread" self.got_history = False self.history_needs_update = False self.team = self.parent_channel.team self.last_line_from = None self.new_messages = False self.buffer_name_needs_update = False @property def members(self): return self.parent_channel.members @property def parent_message(self): return self.parent_channel.messages[self.thread_ts] @property def hashed_messages(self): return self.parent_channel.hashed_messages @property def last_read(self): return self.parent_message.last_read @last_read.setter def last_read(self, ts): self.parent_message.last_read = ts @property def identifier(self): return self.parent_channel.identifier @property def visible_messages(self): return self.messages @property def muted(self): return self.parent_channel.muted @property def pending_history_requests(self): if self.thread_ts in self.parent_channel.pending_history_requests: return {self.identifier, self.thread_ts} else: return set() def formatted_name(self, style="default"): name = self.label_full or self.parent_message.hash if style == "sidebar": name = self.label_short or name if self.label_short_drop_prefix: return name else: indent_expr = w.config_string(w.config_get("buflist.format.indent")) # Only indent with space if slack_type isn't mentioned in the indent option indent = "" if "slack_type" in indent_expr else " " return "{}${}".format(indent, name) elif style == "long_default": if self.label_full_drop_prefix: return name else: channel_name = self.parent_channel.formatted_name(style="long_default") return "{}.{}".format(channel_name, name) else: if self.label_full_drop_prefix: return name else: channel_name = self.parent_channel.formatted_name() return "{}.{}".format(channel_name, name) def mark_read(self, ts=None, update_remote=True, force=False, post_data={}): if not self.parent_message.subscribed: return args = {"thread_ts": self.thread_ts} args.update(post_data) super(SlackThreadChannel, self).mark_read( ts=ts, update_remote=update_remote, force=force, post_data=args ) def buffer_prnt( self, nick, text, timestamp, tagset, tag_nick=None, history_message=False, no_log=False, extra_tags=None, ): data = "{}\t{}".format(format_nick(nick, self.last_line_from), text) self.last_line_from = nick ts = SlackTS(timestamp) if self.channel_buffer: # backlog messages - we will update the read marker as we print these backlog = ts <= self.last_read if not backlog: self.new_messages = True no_log = no_log or history_message and backlog self_msg = tag_nick == self.team.nick tags = tag( ts, tagset, user=tag_nick, self_msg=self_msg, backlog=backlog, no_log=no_log, extra_tags=extra_tags, ) if no_log: w.buffer_set(self.channel_buffer, "print_hooks_enabled", "0") w.prnt_date_tags(self.channel_buffer, ts.major, tags, data) if no_log: w.buffer_set(self.channel_buffer, "print_hooks_enabled", "1") if backlog or self_msg: self.mark_read(ts, update_remote=False, force=True) def get_history(self, slow_queue=False, full=False, no_log=False): self.got_history = True self.history_needs_update = False any_msg_is_none = any(message is None for message in self.messages.values()) if not any_msg_is_none: self.reprint_messages(history_message=True, no_log=no_log) if ( full or any_msg_is_none or len(self.parent_message.submessages) < self.parent_message.number_of_replies() ): self.parent_channel.get_thread_history(self.thread_ts, slow_queue, no_log) def send_message(self, message, subtype=None, request_dict_ext={}): if subtype == "me_message": w.prnt("", "ERROR: /me is not supported in threads") return w.WEECHAT_RC_ERROR request = {"thread_ts": str(self.thread_ts)} request.update(request_dict_ext) super(SlackThreadChannel, self).send_message(message, subtype, request) def open(self, update_remote=True): self.create_buffer() self.active = True self.get_history() def refresh(self): if self.buffer_name_needs_update: self.buffer_name_needs_update = False self.rename() def rename(self): if self.channel_buffer: self.buffer_rename_in_progress = True w.buffer_set( self.channel_buffer, "name", self.formatted_name(style="long_default") ) w.buffer_set( self.channel_buffer, "short_name", self.formatted_name(style="sidebar") ) self.buffer_rename_in_progress = False def set_highlights(self, highlight_string=None): if self.channel_buffer: if highlight_string is None: highlight_string = ",".join(self.parent_channel.highlights()) w.buffer_set(self.channel_buffer, "highlight_words", highlight_string) def create_buffer(self): """ Creates the WeeChat buffer where the thread magic happens. """ if not self.channel_buffer: self.channel_buffer = w.buffer_new( self.formatted_name(style="long_default"), "buffer_input_callback", "EVENTROUTER", "", "", ) self.eventrouter.weechat_controller.register_buffer( self.channel_buffer, self ) w.buffer_set(self.channel_buffer, "input_prompt", self.team.nick) w.buffer_set(self.channel_buffer, "input_multiline", "1") w.buffer_set( self.channel_buffer, "localvar_set_type", get_localvar_type(self.parent_channel.type), ) w.buffer_set(self.channel_buffer, "localvar_set_slack_type", self.type) w.buffer_set(self.channel_buffer, "localvar_set_nick", self.team.nick) w.buffer_set( self.channel_buffer, "localvar_set_channel", self.formatted_name() ) w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.name) w.buffer_set( self.channel_buffer, "localvar_set_completion_default_template", "${weechat.completion.default_template}|%(usergroups)|%(emoji)", ) self.buffer_rename_in_progress = True w.buffer_set( self.channel_buffer, "short_name", self.formatted_name(style="sidebar") ) self.buffer_rename_in_progress = False self.set_highlights() time_format = w.string_eval_expression( w.config_string(w.config_get("weechat.look.buffer_time_format")), {}, {}, {}, ) parent_time = time.localtime(SlackTS(self.thread_ts).major) topic = "{} {} | {}".format( time.strftime(time_format, parent_time), self.parent_message.sender, self.render(self.parent_message), ) w.buffer_set(self.channel_buffer, "title", topic) def destroy_buffer(self, update_remote): super(SlackThreadChannel, self).destroy_buffer(update_remote) if update_remote and not self.eventrouter.shutting_down: self.mark_read() def render(self, message, force=False): return message.render(force) class SlackThreadChannelMessages(MappingReversible): """ Class with a reversible mapping interface (like a read-only OrderedDict) which looks up messages using the parent channel and parent message. """ def __init__(self, thread_channel): self.thread_channel = thread_channel @property def _parent_message(self): return self.thread_channel.parent_message def __getitem__(self, key): if ( key != self._parent_message.ts and key not in self._parent_message.submessages ): raise KeyError(key) return self.thread_channel.parent_channel.messages[key] def __iter__(self): yield self._parent_message.ts for ts in self._parent_message.submessages: yield ts def __len__(self): return 1 + len(self._parent_message.submessages) def __reversed__(self): for ts in reversed(self._parent_message.submessages): yield ts yield self._parent_message.ts class SlackUser(object): """ Represends an individual slack user. Also where you set their name formatting. """ def __init__(self, originating_team_id, **kwargs): self.identifier = kwargs["id"] # These attributes may be missing in the response, so we have to make # sure they're set self.profile = {} self.presence = kwargs.get("presence", "unknown") self.deleted = kwargs.get("deleted", False) self.is_external = ( not kwargs.get("is_bot") and kwargs.get("team_id") != originating_team_id ) for key, value in kwargs.items(): setattr(self, key, value) self.name = nick_from_profile(self.profile, kwargs["name"]) self.username = kwargs["name"] self.update_color() def __repr__(self): return "Name:{} Identifier:{}".format(self.name, self.identifier) def force_color(self, color_name): self.color_name = color_name def update_color(self): # This will automatically be none/"" if the user has disabled nick # colourization. self.color_name = get_nick_color(self.name) def update_status(self, status_emoji, status_text): self.profile["status_emoji"] = status_emoji self.profile["status_text"] = status_text def formatted_name(self, prepend="", enable_color=True): name = prepend + self.name if enable_color: return colorize_string(self.color_name, name) else: return name class SlackBot(SlackUser): """ Basically the same as a user, but split out to identify and for future needs """ def __init__(self, originating_team_id, **kwargs): super(SlackBot, self).__init__(originating_team_id, **kwargs) class SlackMessage(object): """ Represents a single slack message and associated context/metadata. These are modifiable and can be rerendered to change a message, delete a message, add a reaction, add a thread. Note: these can't be tied to a SlackUser object because users can be deleted, so we have to store sender in each one. """ def __init__(self, subtype, message_json, channel): self.team = channel.team self.channel = channel self.subtype = subtype self.user_identifier = message_json.get("user") self.message_json = message_json self.submessages = [] self.ts = SlackTS(message_json["ts"]) self.subscribed = message_json.get("subscribed", False) self.last_read = SlackTS(message_json.get("last_read", 0)) self.last_notify = SlackTS(0) def __hash__(self): return hash(self.ts) @property def hash(self): return self.channel.hashed_messages[self.ts] @property def thread_channel(self): return self.channel.thread_channels.get(self.ts) def open_thread(self, switch=False): if not self.thread_channel or not self.thread_channel.active: self.channel.thread_channels[self.ts] = SlackThreadChannel( EVENTROUTER, self.channel, self.ts ) self.thread_channel.open() if switch: w.buffer_set(self.thread_channel.channel_buffer, "display", "1") def render(self, force=False): # If we already have a rendered version in the object, just return that. if not force and self.message_json.get("_rendered_text"): return self.message_json["_rendered_text"] if self.message_json.get("deleted"): text = colorize_string(config.color_deleted, "(deleted)") self.message_json["_rendered_text"] = text return text blocks = self.message_json.get("blocks", []) blocks_rendered = "\n".join(unfurl_blocks(blocks)) if blocks_rendered: text = blocks_rendered else: text = unhtmlescape(unfurl_refs(self.message_json.get("text", ""))) if self.message_json.get("mrkdwn", True): text = render_formatting(text) if self.message_json.get("subtype") in ( "channel_join", "group_join", ) and self.message_json.get("inviter"): inviter_id = self.message_json.get("inviter") text += unfurl_refs(" by invitation from <@{}>".format(inviter_id)) if self.subtype == "me_message" and not self.message_json["text"].startswith( self.sender ): text = "{} {}".format(self.sender, text) if "edited" in self.message_json: text += " " + colorize_string(config.color_edited_suffix, "(edited)") text += unwrap_attachments(self, text) text += unhtmlescape(unfurl_refs(unwrap_files(self, self.message_json, text))) text += unwrap_huddle(self, self.message_json, text) text = text.lstrip().replace("\t", " ") text += create_reactions_string( self.message_json.get("reactions", ""), self.team.myidentifier ) if self.number_of_replies(): text += " " + colorize_string( get_thread_color(self.hash), "[ Thread: {} Replies: {}{} ]".format( self.hash, self.number_of_replies(), " Subscribed" if self.subscribed else "", ), ) # replace_string_with_emoji() was called on blocks earlier via # unfurl_blocks(), so exclude them here text_to_replace = text[len(blocks_rendered) :] text = text[: len(blocks_rendered)] + replace_string_with_emoji(text_to_replace) self.message_json["_rendered_text"] = text return text def get_sender(self, plain): user = self.team.users.get(self.user_identifier) if user: name = "{}".format(user.formatted_name(enable_color=not plain)) if user.is_external: name += config.external_user_suffix return name elif "user_profile" in self.message_json: nick = nick_from_profile( self.message_json["user_profile"], self.user_identifier ) color_name = get_nick_color(nick) name = nick if plain else colorize_string(color_name, nick) if self.message_json.get("user_team") != self.message_json.get("team"): name += config.external_user_suffix return name elif "username" in self.message_json: username = self.message_json["username"] if plain: return username elif self.message_json.get("subtype") == "bot_message": return "{} :]".format(username) else: return "-{}-".format(username) elif "service_name" in self.message_json: service_name = self.message_json["service_name"] if plain: return service_name else: return "-{}-".format(service_name) elif self.message_json.get("bot_id") in self.team.bots: bot = self.team.bots[self.message_json["bot_id"]] name = bot.formatted_name(enable_color=not plain) if plain: return name else: return "{} :]".format(name) return self.user_identifier or self.message_json.get("bot_id") or "" @property def sender(self): return self.get_sender(False) @property def sender_plain(self): return self.get_sender(True) def get_reaction(self, reaction_name): for reaction in self.message_json.get("reactions", []): if reaction["name"] == reaction_name: return reaction return None def add_reaction(self, reaction_name, user): reaction = self.get_reaction(reaction_name) if reaction: reaction["count"] += 1 if user not in reaction["users"]: reaction["users"].append(user) else: if "reactions" not in self.message_json: self.message_json["reactions"] = [] self.message_json["reactions"].append( {"name": reaction_name, "count": 1, "users": [user]} ) def remove_reaction(self, reaction_name, user): reaction = self.get_reaction(reaction_name) reaction["count"] -= 1 if user in reaction["users"]: reaction["users"].remove(user) def has_mention(self): return w.string_has_highlight( unfurl_refs(self.message_json.get("text")), ",".join(self.channel.highlights()), ) def number_of_replies(self): return max(len(self.submessages), self.message_json.get("reply_count", 0)) def notify_thread(self, message=None): if message is None: if not self.submessages: return message = self.channel.messages.get(self.submessages[-1]) if ( self.thread_channel and self.thread_channel.active or message.ts <= self.last_read or message.ts <= self.last_notify ): return if message.has_mention(): template = "You were mentioned in thread {hash}, channel {channel}" elif self.subscribed: template = "New message in thread {hash}, channel {channel} to which you are subscribed" else: return self.last_notify = max(message.ts, SlackTS()) if config.auto_open_threads and self.subscribed: self.open_thread() if message.user_identifier != self.team.myidentifier and ( config.notify_subscribed_threads is True or config.notify_subscribed_threads == "auto" and not config.auto_open_threads and not config.thread_messages_in_channel ): message = template.format( hash=self.hash, channel=self.channel.formatted_name() ) self.team.buffer_prnt(message, message=True) class SlackThreadMessage(SlackMessage): def __init__(self, parent_channel, thread_ts, message_json, *args): subtype = message_json.get( "subtype", "thread_broadcast" if message_json.get("reply_broadcast") else "thread_message", ) super(SlackThreadMessage, self).__init__(subtype, message_json, *args) self.parent_channel = parent_channel self.thread_ts = thread_ts @property def parent_message(self): return self.parent_channel.messages.get(self.thread_ts) def open_thread(self, switch=False): self.parent_message.open_thread(switch) class Hdata(object): def __init__(self, w): self.buffer = w.hdata_get("buffer") self.line = w.hdata_get("line") self.line_data = w.hdata_get("line_data") self.lines = w.hdata_get("lines") class SlackTS(object): def __init__(self, ts=None): if isinstance(ts, int): self.major = ts self.minor = 0 elif ts is not None: self.major, self.minor = [int(x) for x in ts.split(".", 1)] else: self.major = int(time.time()) self.minor = 0 def __cmp__(self, other): if isinstance(other, SlackTS): if self.major < other.major: return -1 elif self.major > other.major: return 1 elif self.major == other.major: if self.minor < other.minor: return -1 elif self.minor > other.minor: return 1 else: return 0 elif isinstance(other, str): s = self.__str__() if s < other: return -1 elif s > other: return 1 elif s == other: return 0 def __lt__(self, other): return self.__cmp__(other) < 0 def __le__(self, other): return self.__cmp__(other) <= 0 def __eq__(self, other): return self.__cmp__(other) == 0 def __ne__(self, other): return self.__cmp__(other) != 0 def __ge__(self, other): return self.__cmp__(other) >= 0 def __gt__(self, other): return self.__cmp__(other) > 0 def __hash__(self): return hash("{}.{}".format(self.major, self.minor)) def __repr__(self): return str("{0}.{1:06d}".format(self.major, self.minor)) def split(self, *args, **kwargs): return [self.major, self.minor] def majorstr(self): return str(self.major) def minorstr(self): return str(self.minor) ###### New handlers def handle_rtmstart(login_data, eventrouter, team, channel, metadata): """ This handles the main entry call to slack, rtm.start """ metadata = login_data["wee_slack_request_metadata"] if not login_data["ok"]: w.prnt( "", "ERROR: Failed connecting to Slack with token {}: {}".format( token_for_print(metadata.token), login_data["error"] ), ) if not re.match(r"^xo\w\w(-\d+){3}-[0-9a-f]+$", metadata.token): w.prnt( "", "ERROR: Token does not look like a valid Slack token. " "Ensure it is a valid token and not just a OAuth code.", ) return self_profile = next( user["profile"] for user in login_data["users"] if user["id"] == login_data["self"]["id"] ) self_nick = nick_from_profile(self_profile, login_data["self"]["name"]) # Let's reuse a team if we have it already. th = SlackTeam.generate_team_hash( login_data["team"]["id"], login_data["team"]["domain"] ) if not eventrouter.teams.get(th): users = {} for item in login_data["users"]: users[item["id"]] = SlackUser(login_data["team"]["id"], **item) bots = {} for item in login_data["bots"]: bots[item["id"]] = SlackBot(login_data["team"]["id"], **item) subteams = {} for item in login_data["subteams"]["all"]: is_member = item["id"] in login_data["subteams"]["self"] subteams[item["id"]] = SlackSubteam( login_data["team"]["id"], is_member=is_member, **item ) channels = {} for item in login_data["channels"]: if item["is_shared"]: channels[item["id"]] = SlackSharedChannel(eventrouter, **item) elif item["is_mpim"]: channels[item["id"]] = SlackMPDMChannel( eventrouter, users, login_data["self"]["id"], **item ) elif item["is_private"]: channels[item["id"]] = SlackPrivateChannel(eventrouter, **item) else: channels[item["id"]] = SlackChannel(eventrouter, **item) for item in login_data["ims"]: channels[item["id"]] = SlackDMChannel( eventrouter, users, login_data["self"]["id"], **item ) for item in login_data["mpims"]: channels[item["id"]] = SlackMPDMChannel( eventrouter, users, login_data["self"]["id"], **item ) for item in login_data["groups"]: if not item["is_mpim"]: channels[item["id"]] = SlackGroupChannel(eventrouter, **item) t = SlackTeam( eventrouter, metadata.token, th, login_data["url"], login_data["team"], subteams, self_nick, login_data["self"]["id"], login_data["self"]["manual_presence"], users, bots, channels, muted_channels=login_data["self"]["prefs"]["muted_channels"], highlight_words=login_data["self"]["prefs"]["highlight_words"], ) eventrouter.register_team(t) else: t = eventrouter.teams.get(th) if t.myidentifier != login_data["self"]["id"]: print_error( "The Slack team {} has tokens for two different users, this is not supported. The " "token {} is for user {}, and the token {} is for user {}. Please remove one of " "them.".format( t.team_info["name"], token_for_print(t.token), t.nick, token_for_print(metadata.token), self_nick, ) ) return elif not metadata.metadata.get("reconnect"): print_error( "Ignoring duplicate Slack tokens for the same team ({}) and user ({}). The two " "tokens are {} and {}.".format( t.team_info["name"], t.nick, token_for_print(t.token), token_for_print(metadata.token), ), warning=True, ) return else: t.set_reconnect_url(login_data["url"]) t.connecting_rtm = False t.connect() def handle_rtmconnect(login_data, eventrouter, team, channel, metadata): metadata = login_data["wee_slack_request_metadata"] team = metadata.team team.connecting_rtm = False if not login_data["ok"]: w.prnt( "", "ERROR: Failed reconnecting to Slack with token {}: {}".format( token_for_print(metadata.token), login_data["error"] ), ) return team.set_reconnect_url(login_data["url"]) team.connect() def handle_emojilist(emoji_json, eventrouter, team, channel, metadata): if emoji_json["ok"]: team.emoji_completions.extend(emoji_json["emoji"].keys()) def handle_conversationsinfo(channel_json, eventrouter, team, channel, metadata): channel_info = channel_json["channel"] if "unread_count_display" in channel_info: unread_count = channel_info["unread_count_display"] if unread_count and channel.channel_buffer is None: channel.create_buffer() channel.set_unread_count_display(unread_count) if channel_info.get("is_open") and channel.channel_buffer is None: channel.create_buffer() if "last_read" in channel_info: channel.last_read = SlackTS(channel_info["last_read"]) if "members" in channel_info: channel.set_members(channel_info["members"]) # MPIMs don't have unread_count_display so we have to request the history to check if there are unread messages if channel.type == "mpim" and not channel.got_history: s = SlackRequest( team, "conversations.history", {"channel": channel.identifier, "limit": 1}, channel=channel, metadata={"only_set_unread": True}, ) eventrouter.receive(s) def handle_conversationsopen( conversation_json, eventrouter, team, channel, metadata, object_name="channel" ): channel_info = conversation_json[object_name] if not channel: channel = create_channel_from_info( eventrouter, channel_info, team, team.myidentifier, team.users ) team.channels[channel_info["id"]] = channel if channel.channel_buffer is None: channel.create_buffer() unread_count_display = channel_info.get("unread_count_display") if unread_count_display is not None: channel.set_unread_count_display(unread_count_display) if metadata.get("switch") and config.switch_buffer_on_join: w.buffer_set(channel.channel_buffer, "display", "1") def handle_mpimopen( mpim_json, eventrouter, team, channel, metadata, object_name="group" ): handle_conversationsopen( mpim_json, eventrouter, team, channel, metadata, object_name ) def handle_history( message_json, eventrouter, team, channel, metadata, includes_threads=True ): if metadata.get("only_set_unread"): if message_json["messages"]: latest = message_json["messages"][0] latest_ts = SlackTS(latest["ts"]) if latest_ts > channel.last_read: if not channel.channel_buffer: channel.create_buffer() channel.set_unread_count_display(1) return channel.got_history = True channel.history_needs_update = False for message in reversed(message_json["messages"]): message = process_message( message, eventrouter, team, channel, metadata, history_message=True ) if ( not includes_threads and message and message.number_of_replies() and ( config.thread_messages_in_channel or message.subscribed and SlackTS(message.message_json.get("latest_reply", 0)) > message.last_read ) ): channel.get_thread_history( message.ts, metadata["slow_queue"], metadata["no_log"] ) channel.pending_history_requests.discard(channel.identifier) if ( channel.visible_messages.first_ts_to_display.major == 0 and message_json["messages"] ): channel.visible_messages.first_ts_to_display = SlackTS( message_json["messages"][-1]["ts"] ) channel.reprint_messages(history_message=True, no_log=metadata["no_log"]) for thread_channel in channel.thread_channels.values(): thread_channel.reprint_messages(history_message=True, no_log=metadata["no_log"]) handle_channelshistory = handle_history handle_groupshistory = handle_history handle_imhistory = handle_history handle_mpimhistory = handle_history def handle_conversationshistory( message_json, eventrouter, team, channel, metadata, includes_threads=True ): handle_history(message_json, eventrouter, team, channel, metadata, False) def handle_conversationsreplies(message_json, eventrouter, team, channel, metadata): for message in message_json["messages"]: process_message( message, eventrouter, team, channel, metadata, history_message=True ) channel.pending_history_requests.discard(metadata.get("thread_ts")) thread_channel = channel.thread_channels.get(metadata.get("thread_ts")) if thread_channel and thread_channel.active: thread_channel.got_history = True thread_channel.history_needs_update = False thread_channel.reprint_messages(history_message=True, no_log=metadata["no_log"]) if config.thread_messages_in_channel: channel.reprint_messages(history_message=True, no_log=metadata["no_log"]) def handle_conversationsmembers(members_json, eventrouter, team, channel, metadata): if members_json["ok"]: channel.got_members = True channel.set_members(members_json["members"]) unknown_users = set(members_json["members"]) - set(team.users.keys()) for user in unknown_users: s = SlackRequest(team, "users.info", {"user": user}, channel=channel) eventrouter.receive(s) if channel.type == "mpim": name = channel.name_from_members() channel.set_name(name) else: w.prnt( team.channel_buffer, "{}Couldn't load members for channel {}. Error: {}".format( w.prefix("error"), channel.name, members_json["error"] ), ) def handle_usersinfo(user_json, eventrouter, team, channel, metadata): user_info = user_json["user"] if not metadata.get("user"): user = SlackUser(team.identifier, **user_info) team.users[user_info["id"]] = user if channel.type == "shared": channel.update_nicklist(user_info["id"]) elif channel.type == "im": channel.set_name(user.name) channel.set_topic(create_user_status_string(user.profile)) def handle_usergroupsuserslist(users_json, eventrouter, team, channel, metadata): header = "Users in {}".format(metadata["usergroup_handle"]) users = [team.users[key] for key in users_json["users"]] return print_users_info(team, header, users) def handle_usersprofileset(json, eventrouter, team, channel, metadata): if not json["ok"]: w.prnt("", "ERROR: Failed to set profile: {}".format(json["error"])) def handle_conversationscreate(json, eventrouter, team, channel, metadata): metadata = json["wee_slack_request_metadata"] if not json["ok"]: name = metadata.post_data["name"] print_error("Couldn't create channel {}: {}".format(name, json["error"])) def handle_conversationsinvite(json, eventrouter, team, channel, metadata): nicks = ", ".join(metadata["nicks"]) if json["ok"]: w.prnt(team.channel_buffer, "Invited {} to {}".format(nicks, channel.name)) else: w.prnt( team.channel_buffer, "ERROR: Couldn't invite {} to {}. Error: {}".format( nicks, channel.name, json["error"] ), ) def handle_chatcommand(json, eventrouter, team, channel, metadata): command = "{} {}".format(metadata["command"], metadata["command_args"]).rstrip() response = unfurl_refs(json["response"]) if "response" in json else "" if json["ok"]: response_text = "Response: {}".format(response) if response else "No response" w.prnt( team.channel_buffer, 'Ran command "{}". {}'.format(command, response_text) ) else: response_text = ". Response: {}".format(response) if response else "" w.prnt( team.channel_buffer, 'ERROR: Couldn\'t run command "{}". Error: {}{}'.format( command, json["error"], response_text ), ) def handle_chatdelete(json, eventrouter, team, channel, metadata): if not json["ok"]: print_error("Couldn't delete message: {}".format(json["error"])) def handle_chatupdate(json, eventrouter, team, channel, metadata): if not json["ok"]: print_error("Couldn't change message: {}".format(json["error"])) def handle_reactionsadd(json, eventrouter, team, channel, metadata): if not json["ok"]: print_error( "Couldn't add reaction {}: {}".format(metadata["reaction"], json["error"]) ) def handle_reactionsremove(json, eventrouter, team, channel, metadata): if not json["ok"]: print_error( "Couldn't remove reaction {}: {}".format( metadata["reaction"], json["error"] ) ) def handle_subscriptionsthreadmark(json, eventrouter, team, channel, metadata): if not json["ok"]: if json["error"] == "not_allowed_token_type": team.slack_api_translator["thread"]["mark"] = None else: print_error("Couldn't set thread read status: {}".format(json["error"])) def handle_subscriptionsthreadadd(json, eventrouter, team, channel, metadata): if not json["ok"]: if json["error"] == "not_allowed_token_type": print_error( "Can only subscribe to a thread when using a session token, see the readme: https://github.com/wee-slack/wee-slack#4-add-your-slack-api-tokens" ) else: print_error("Couldn't add thread subscription: {}".format(json["error"])) def handle_subscriptionsthreadremove(json, eventrouter, team, channel, metadata): if not json["ok"]: if json["error"] == "not_allowed_token_type": print_error( "Can only unsubscribe from a thread when using a session token, see the readme: https://github.com/wee-slack/wee-slack#4-add-your-slack-api-tokens" ) else: print_error("Couldn't remove thread subscription: {}".format(json["error"])) ###### New/converted process_ and subprocess_ methods def process_hello(message_json, eventrouter, team, channel, metadata): team.subscribe_users_presence() def process_reconnect_url(message_json, eventrouter, team, channel, metadata): team.set_reconnect_url(message_json["url"]) def process_presence_change(message_json, eventrouter, team, channel, metadata): users = [team.users[user_id] for user_id in message_json.get("users", [])] if "user" in metadata: users.append(metadata["user"]) for user in users: team.update_member_presence(user, message_json["presence"]) if team.myidentifier in users: w.bar_item_update("away") w.bar_item_update("slack_away") def process_manual_presence_change(message_json, eventrouter, team, channel, metadata): team.my_manual_presence = message_json["presence"] w.bar_item_update("away") w.bar_item_update("slack_away") def process_pref_change(message_json, eventrouter, team, channel, metadata): if message_json["name"] == "muted_channels": team.set_muted_channels(message_json["value"]) elif message_json["name"] == "highlight_words": team.set_highlight_words(message_json["value"]) elif message_json["name"] == "all_notifications_prefs": new_prefs = json.loads(message_json["value"]) new_muted_channels = set( channel_id for channel_id, prefs in new_prefs["channels"].items() if prefs["muted"] ) team.set_muted_channels(",".join(new_muted_channels)) global_keywords = new_prefs["global"]["global_keywords"] team.set_highlight_words(global_keywords) else: dbg("Preference change not implemented: {}\n".format(message_json["name"])) def process_user_change(message_json, eventrouter, team, channel, metadata): """ Currently only used to update status, but lots here we could do. """ user = metadata["user"] profile = message_json["user"]["profile"] if user: user.update_status(profile.get("status_emoji"), profile.get("status_text")) dmchannel = team.find_channel_by_members( {team.myidentifier, user.identifier}, channel_type="im" ) if dmchannel: dmchannel.set_topic(create_user_status_string(profile)) def process_user_typing(message_json, eventrouter, team, channel, metadata): if channel and metadata["user"]: channel.set_typing(metadata["user"]) w.bar_item_update("slack_typing_notice") def process_team_join(message_json, eventrouter, team, channel, metadata): user = message_json["user"] team.users[user["id"]] = SlackUser(team.identifier, **user) def process_pong(message_json, eventrouter, team, channel, metadata): team.last_pong_time = time.time() def process_message( message_json, eventrouter, team, channel, metadata, history_message=False ): if channel is None: return subtype = message_json.get("subtype") if ( not history_message and not subtype and "ts" in message_json and SlackTS(message_json["ts"]) in channel.messages ): return subtype_functions = get_functions_with_prefix("subprocess_") if "thread_ts" in message_json and "reply_count" not in message_json: message = subprocess_thread_message( message_json, eventrouter, team, channel, history_message ) elif subtype in subtype_functions: message = subtype_functions[subtype]( message_json, eventrouter, team, channel, history_message ) else: message = SlackMessage(subtype or "normal", message_json, channel) channel.store_message(message) channel.unread_count_display += 1 if message and not history_message: channel.prnt_message(message, history_message) if not history_message: download_files(message_json, channel) return message def download_files(message_json, channel): download_location = config.files_download_location if not download_location: return options = { "directory": "data", } download_location = w.string_eval_path_home(download_location, {}, {}, options) if not os.path.exists(download_location): try: os.makedirs(download_location) except: w.prnt( "", "ERROR: Failed to create directory at files_download_location: {}".format( format_exc_only() ), ) def fileout_iter(path): yield path main, ext = os.path.splitext(path) for i in count(start=1): yield main + "-{}".format(i) + ext for f in message_json.get("files", []): if f.get("mode") == "tombstone": continue filetype = "" if f["title"].endswith(f["filetype"]) else "." + f["filetype"] filename = "{}.{}_{}{}".format( channel.team.name, channel.name, f["title"], filetype ) for fileout in fileout_iter(os.path.join(download_location, filename)): if os.path.isfile(fileout): continue curl_options = SlackRequest(channel.team, "").options() curl_options["file_out"] = fileout w.hook_process_hashtable( "url:" + f["url_private"], curl_options, config.slack_timeout, "", "", ) break def subprocess_thread_message( message_json, eventrouter, team, channel, history_message ): parent_ts = SlackTS(message_json["thread_ts"]) message = SlackThreadMessage(channel, parent_ts, message_json, channel) parent_message = message.parent_message if parent_message and message.ts not in parent_message.submessages: parent_message.submessages.append(message.ts) parent_message.submessages.sort() channel.store_message(message) if parent_message: channel.change_message(parent_ts) if parent_message.thread_channel and parent_message.thread_channel.active: if not history_message: parent_message.thread_channel.prnt_message(message, history_message) else: parent_message.notify_thread(message) else: channel.get_thread_history(parent_ts) return message subprocess_thread_broadcast = subprocess_thread_message def subprocess_channel_join(message_json, eventrouter, team, channel, history_message): message = SlackMessage("join", message_json, channel) channel.store_message(message) channel.user_joined(message_json["user"]) return message def subprocess_channel_leave(message_json, eventrouter, team, channel, history_message): message = SlackMessage("leave", message_json, channel) channel.store_message(message) channel.user_left(message_json["user"]) return message def subprocess_channel_topic(message_json, eventrouter, team, channel, history_message): message = SlackMessage("topic", message_json, channel) channel.store_message(message) channel.set_topic(message_json["topic"]) return message subprocess_group_join = subprocess_channel_join subprocess_group_leave = subprocess_channel_leave subprocess_group_topic = subprocess_channel_topic def subprocess_message_replied( message_json, eventrouter, team, channel, history_message ): pass def subprocess_message_changed( message_json, eventrouter, team, channel, history_message ): new_message = message_json.get("message") channel.change_message(new_message["ts"], message_json=new_message) def subprocess_message_deleted( message_json, eventrouter, team, channel, history_message ): channel.change_message(message_json["deleted_ts"], {"deleted": True}) def process_reply(message_json, eventrouter, team, channel, metadata): reply_to = int(message_json["reply_to"]) original_message_json = team.ws_replies.pop(reply_to, None) if original_message_json: dbg("REPLY {}".format(message_json)) channel = team.channels[original_message_json.get("channel")] if message_json["ok"]: original_message_json.update(message_json) process_message( original_message_json, eventrouter, team=team, channel=channel, metadata={}, ) else: print_error( "Couldn't send message to channel {}: {}".format( channel.name, message_json["error"] ) ) else: dbg("Unexpected reply {}".format(message_json)) def process_channel_marked(message_json, eventrouter, team, channel, metadata): ts = message_json.get("ts") if ts and channel is not None: channel.mark_read(ts=ts, force=True, update_remote=False) else: dbg("tried to mark something weird {}".format(message_json)) process_group_marked = process_channel_marked process_im_marked = process_channel_marked process_mpim_marked = process_channel_marked def process_thread_marked(message_json, eventrouter, team, channel, metadata): subscription = message_json.get("subscription", {}) ts = subscription.get("last_read") thread_ts = subscription.get("thread_ts") channel = team.channels.get(subscription.get("channel")) if ts and thread_ts and channel: thread_channel = channel.thread_channels.get(SlackTS(thread_ts)) if thread_channel: thread_channel.mark_read(ts=ts, force=True, update_remote=False) else: dbg("tried to mark something weird {}".format(message_json)) def process_channel_joined(message_json, eventrouter, team, channel, metadata): if channel is None: channel = create_channel_from_info( eventrouter, message_json["channel"], team, team.myidentifier, team.users ) team.channels[message_json["channel"]["id"]] = channel else: channel.update_from_message_json(message_json["channel"]) channel.open() def process_channel_created(message_json, eventrouter, team, channel, metadata): item = message_json["channel"] item["is_member"] = False channel = SlackChannel(eventrouter, team=team, **item) team.channels[item["id"]] = channel if config.log_channel_created: team.buffer_prnt("Channel created: {}".format(channel.name)) def process_channel_rename(message_json, eventrouter, team, channel, metadata): if channel is None: return channel.set_name(message_json["channel"]["name"]) def process_im_created(message_json, eventrouter, team, channel, metadata): item = message_json["channel"] channel = SlackDMChannel( eventrouter, team.users, team.myidentifier, team=team, **item ) team.channels[item["id"]] = channel team.buffer_prnt("IM channel created: {}".format(channel.name)) def process_im_open(message_json, eventrouter, team, channel, metadata): channel.check_should_open(True) w.buffer_set(channel.channel_buffer, "hotlist", "2") def process_im_close(message_json, eventrouter, team, channel, metadata): if channel.channel_buffer: w.prnt( team.channel_buffer, "IM {} closed by another client or the server".format(channel.name), ) eventrouter.weechat_controller.unregister_buffer( channel.channel_buffer, False, True ) def process_mpim_joined(message_json, eventrouter, team, channel, metadata): item = message_json["channel"] channel = SlackMPDMChannel( eventrouter, team.users, team.myidentifier, team=team, **item ) team.channels[item["id"]] = channel channel.open() def process_group_joined(message_json, eventrouter, team, channel, metadata): item = message_json["channel"] if item["is_mpim"]: return channel = SlackGroupChannel(eventrouter, team=team, **item) team.channels[item["id"]] = channel channel.open() def process_reaction_added(message_json, eventrouter, team, channel, metadata): channel = team.channels.get(message_json["item"].get("channel")) if channel is None: return if message_json["item"].get("type") == "message": ts = SlackTS(message_json["item"]["ts"]) message = channel.messages.get(ts) if message: message.add_reaction(message_json["reaction"], message_json["user"]) channel.change_message(ts) else: dbg("reaction to item type not supported: " + str(message_json)) def process_reaction_removed(message_json, eventrouter, team, channel, metadata): channel = team.channels.get(message_json["item"].get("channel")) if channel is None: return if message_json["item"].get("type") == "message": ts = SlackTS(message_json["item"]["ts"]) message = channel.messages.get(ts) if message: message.remove_reaction(message_json["reaction"], message_json["user"]) channel.change_message(ts) else: dbg("Reaction to item type not supported: " + str(message_json)) def process_subteam_created(subteam_json, eventrouter, team, channel, metadata): subteam_json_info = subteam_json["subteam"] is_member = team.myidentifier in subteam_json_info.get("users", []) subteam = SlackSubteam(team.identifier, is_member=is_member, **subteam_json_info) team.subteams[subteam_json_info["id"]] = subteam def process_subteam_updated(subteam_json, eventrouter, team, channel, metadata): current_subteam_info = team.subteams.get(subteam_json["subteam"]["id"]) if current_subteam_info is None: return is_member = team.myidentifier in subteam_json["subteam"].get("users", []) new_subteam_info = SlackSubteam( team.identifier, is_member=is_member, **subteam_json["subteam"] ) team.subteams[subteam_json["subteam"]["id"]] = new_subteam_info if current_subteam_info.is_member != new_subteam_info.is_member: for channel in team.channels.values(): channel.set_highlights() if ( config.notify_usergroup_handle_updated and current_subteam_info.handle != new_subteam_info.handle ): message = "User group {old_handle} has updated its handle to {new_handle} in team {team}.".format( old_handle=current_subteam_info.handle, new_handle=new_subteam_info.handle, team=team.name, ) team.buffer_prnt(message, message=True) def process_emoji_changed(message_json, eventrouter, team, channel, metadata): team.load_emoji_completions() def process_thread_subscribed(message_json, eventrouter, team, channel, metadata): dbg("THREAD SUBSCRIBED {}".format(message_json)) channel = team.channels[message_json["subscription"]["channel"]] parent_ts = SlackTS(message_json["subscription"]["thread_ts"]) parent_message = channel.messages.get(parent_ts) if parent_message: parent_message.last_read = SlackTS(message_json["subscription"]["last_read"]) parent_message.subscribed = True channel.change_message(parent_ts) parent_message.notify_thread() else: channel.get_thread_history(parent_ts) def process_thread_unsubscribed(message_json, eventrouter, team, channel, metadata): dbg("THREAD UNSUBSCRIBED {}".format(message_json)) channel = team.channels[message_json["subscription"]["channel"]] parent_ts = SlackTS(message_json["subscription"]["thread_ts"]) parent_message = channel.messages.get(parent_ts) if parent_message: parent_message.subscribed = False channel.change_message(parent_ts) ###### New module/global methods def render_formatting(text): text = re.sub( r"(^| )\*([^*\n`]+)\*(?=[^\w]|$)", r"\1{}*\2*{}".format( w.color(config.render_bold_as), w.color("-" + config.render_bold_as) ), text, flags=re.UNICODE, ) text = re.sub( r"(^| )_([^_\n`]+)_(?=[^\w]|$)", r"\1{}_\2_{}".format( w.color(config.render_italic_as), w.color("-" + config.render_italic_as) ), text, flags=re.UNICODE, ) return text def linkify_text(message, team, only_users=False, escape_characters=True): # The get_username_map function is a bit heavy, but this whole # function is only called on message send.. usernames = team.get_username_map() channels = team.get_channel_map() usergroups = team.generate_usergroup_map() if escape_characters: message = ( message # Replace IRC formatting chars with Slack formatting chars. .replace("\x02", "*") .replace("\x1D", "_") .replace("\x1F", config.map_underline_to) # Escape chars that have special meaning to Slack. Note that we do not # (and should not) perform full HTML entity-encoding here. # See https://api.slack.com/docs/message-formatting for details. .replace("&", "&") .replace("<", "<") .replace(">", ">") ) def linkify_word(match): word = match.group(0) prefix, name = match.groups() if prefix == "@": if name in ["channel", "everyone", "group", "here"]: return "".format(name) elif name in usernames: return "<@{}>".format(usernames[name]) elif word in usergroups.keys(): return "".format(usergroups[word], word) elif prefix == "#" and not only_users: if word in channels: return "<#{}|{}>".format(channels[word], name) return word linkify_regex = r"(?:^|(?<=\s))([@#])([\w\(\)\'.-]+)" return re.sub(linkify_regex, linkify_word, message, flags=re.UNICODE) def unfurl_blocks(blocks): block_text = [] for block in blocks: try: if block["type"] == "section": fields = block.get("fields", []) if "text" in block: fields.insert(0, block["text"]) block_text.extend(unfurl_block_element(field) for field in fields) elif block["type"] == "actions": elements = [] for element in block["elements"]: if element["type"] == "button": elements.append(unfurl_block_element(element["text"])) if "url" in element: elements.append(element["url"]) else: elements.append( colorize_string( config.color_deleted, '<>'.format( element["type"] ), ) ) block_text.append(" | ".join(elements)) elif block["type"] == "call": block_text.append("Join via " + block["call"]["v1"]["join_url"]) elif block["type"] == "divider": block_text.append("---") elif block["type"] == "context": block_text.append( " | ".join(unfurl_block_element(el) for el in block["elements"]) ) elif block["type"] == "image": if "title" in block: block_text.append(unfurl_block_element(block["title"])) block_text.append(unfurl_block_element(block)) elif block["type"] == "rich_text": for element in block.get("elements", []): if element["type"] == "rich_text_section": rendered = unfurl_rich_text_section(element) if rendered: block_text.append(rendered) elif element["type"] == "rich_text_list": rendered = [ "{}{} {}".format( " " * element.get("indent", 0), block_list_prefix( element, element.get("offset", 0) + i ), unfurl_rich_text_section(e), ) for i, e in enumerate(element["elements"]) ] block_text.extend(rendered) elif element["type"] == "rich_text_quote": lines = [ "> {}".format(line) for e in element["elements"] for line in unfurl_block_rich_text_element(e).split("\n") ] block_text.extend(lines) elif element["type"] == "rich_text_preformatted": texts = [ e.get("text", e.get("url", "")) for e in element["elements"] ] if texts: block_text.append("```\n{}\n```".format("".join(texts))) else: text = '<>'.format( element["type"] ) block_text.append(colorize_string(config.color_deleted, text)) dbg( "Unsupported rich_text element: '{}'".format( json.dumps(element) ), level=4, ) else: block_text.append( colorize_string( config.color_deleted, '<>'.format(block["type"]), ) ) dbg("Unsupported block: '{}'".format(json.dumps(block)), level=4) except Exception as e: dbg( "Failed to unfurl block ({}): {}".format(repr(e), json.dumps(block)), level=4, ) return block_text def convert_int_to_letter(num): letter = "" while num > 0: num -= 1 letter = chr((num % 26) + 97) + letter num //= 26 return letter def convert_int_to_roman(num): roman_numerals = { 1000: "m", 900: "cm", 500: "d", 400: "cd", 100: "c", 90: "xc", 50: "l", 40: "xl", 10: "x", 9: "ix", 5: "v", 4: "iv", 1: "i", } roman_numeral = "" for value, symbol in roman_numerals.items(): while num >= value: roman_numeral += symbol num -= value return roman_numeral def block_list_prefix(element, index): if element["style"] == "ordered": if element["indent"] == 0 or element["indent"] == 3: return "{}.".format(index + 1) elif element["indent"] == 1 or element["indent"] == 4: return "{}.".format(convert_int_to_letter(index + 1)) else: return "{}.".format(convert_int_to_roman(index + 1)) else: if element["indent"] == 0 or element["indent"] == 3: return "•" elif element["indent"] == 1 or element["indent"] == 4: return "◦" else: return "▪︎" def unfurl_rich_text_section(block): texts = [] prev_element = {"type": "text", "text": ""} for element in block["elements"] + [prev_element.copy()]: colors_apply = [] colors_remove = [] characters_apply = [] characters_remove = [] prev_style = prev_element.get("style", {}) cur_style = element.get("style", {}) if cur_style.get("bold", False) != prev_style.get("bold", False): if cur_style.get("bold"): colors_apply.append(w.color(config.render_bold_as)) characters_apply.append("*") else: colors_remove.append(w.color("-" + config.render_bold_as)) characters_remove.append("*") if cur_style.get("italic", False) != prev_style.get("italic", False): if cur_style.get("italic"): colors_apply.append(w.color(config.render_italic_as)) characters_apply.append("_") else: colors_remove.append(w.color("-" + config.render_italic_as)) characters_remove.append("_") if cur_style.get("strike", False) != prev_style.get("strike", False): if cur_style.get("strike"): characters_apply.append("~") else: characters_remove.append("~") if cur_style.get("code", False) != prev_style.get("code", False): if cur_style.get("code"): characters_apply.append("`") else: characters_remove.append("`") texts.extend(reversed(characters_remove)) texts.extend(reversed(colors_remove)) texts.extend(colors_apply) texts.extend(characters_apply) texts.append(unfurl_block_rich_text_element(element)) prev_element = element text = "".join(texts) if text.endswith("\n"): return text[:-1] else: return text def unfurl_block_rich_text_element(element): if element["type"] == "text": return element["text"] elif element["type"] == "link": text = element.get("text") if text and text != element["url"]: if element.get("style", {}).get("code"): return text else: return unfurl_link(element["url"], text) else: return element["url"] elif element["type"] == "emoji": return replace_string_with_emoji(":{}:".format(element["name"])) elif element["type"] == "color": rgb_int = int(element["value"].lstrip("#"), 16) weechat_color = w.info_get("color_rgb2term", str(rgb_int)) return "{} {}".format(element["value"], colorize_string(weechat_color, "■")) elif element["type"] == "user": return resolve_ref("@{}".format(element["user_id"])) elif element["type"] == "usergroup": return resolve_ref("!subteam^{}".format(element["usergroup_id"])) elif element["type"] == "broadcast": return resolve_ref("@{}".format(element["range"])) elif element["type"] == "channel": return resolve_ref("#{}".format(element["channel_id"])) else: dbg("Unsupported rich text element: '{}'".format(json.dumps(element)), level=4) return colorize_string( config.color_deleted, '<>'.format(element["type"]), ) def unfurl_block_element(element): if element["type"] == "mrkdwn": return render_formatting(unhtmlescape(unfurl_refs(element["text"]))) elif element["type"] == "plain_text": return unhtmlescape(unfurl_refs(element["text"])) elif element["type"] == "image": if element.get("alt_text"): return "{} ({})".format(element["image_url"], element["alt_text"]) else: return element["image_url"] else: dbg("Unsupported block element: '{}'".format(json.dumps(element)), level=4) return colorize_string( config.color_deleted, '<>'.format(element["type"]), ) def unfurl_link(url, text): match_url = r"^\w+:(//)?{}$".format(re.escape(text)) url_matches_desc = re.match(match_url, url) if url_matches_desc and config.unfurl_auto_link_display == "text": return text elif url_matches_desc and config.unfurl_auto_link_display == "url": return url else: return "{} ({})".format(url, text) def unfurl_refs(text): """ input : <@U096Q7CQM|someuser> has joined the channel ouput : someuser has joined the channel """ # Find all strings enclosed by <> # - # - <#C2147483705|#otherchannel> # - <@U2147483697|@othernick> # - # Test patterns lives in ./_pytest/test_unfurl.py def unfurl_ref(match): ref, fallback = match.groups() resolved_ref = resolve_ref(ref) if resolved_ref != ref: return resolved_ref if fallback and fallback != ref and not config.unfurl_ignore_alt_text: if ref.startswith("#"): return "#{}".format(fallback) elif ref.startswith("@"): return fallback elif ref.startswith("!subteam"): prefix = "@" if not fallback.startswith("@") else "" return prefix + fallback elif ref.startswith("!date"): return fallback else: return unfurl_link(ref, fallback) return ref return re.sub(r"<([^|>]*)(?:\|([^>]*))?>", unfurl_ref, text) def htmlescape(text): return text.replace("&", "&").replace("<", "<").replace(">", ">") def unhtmlescape(text): return text.replace("<", "<").replace(">", ">").replace("&", "&") def unwrap_attachments(message, text_before): attachment_texts = [] a = message.message_json.get("attachments") if a: if text_before: attachment_texts.append("") for attachment in a: # Attachments should be rendered roughly like: # # $pretext # $author: (if rest of line is non-empty) $title ($title_link) OR $from_url # $author: (if no $author on previous line) $text # $fields if not config.link_previews and ( "original_url" in attachment or attachment.get("is_app_unfurl") ): continue t = [] prepend_title_text = "" if "author_name" in attachment: prepend_title_text = attachment["author_name"] + ": " if "pretext" in attachment: t.append(attachment["pretext"]) link_shown = False title = attachment.get("title") title_link = attachment.get("title_link", "") if title_link and title_link in text_before: title_link = "" link_shown = True if title and title_link: t.append( "%s%s (%s)" % ( prepend_title_text, title, htmlescape(title_link), ) ) prepend_title_text = "" elif title and not title_link: t.append( "%s%s" % ( prepend_title_text, title, ) ) prepend_title_text = "" from_url = attachment.get("from_url", "") if from_url not in text_before and from_url != title_link: t.append(htmlescape(from_url)) elif from_url: link_shown = True atext = attachment.get("text") if atext: tx = re.sub(r" *\n[\n ]+", "\n", atext) t.append(prepend_title_text + tx) prepend_title_text = "" image_url = attachment.get("image_url", "") if ( image_url not in text_before and image_url != from_url and image_url != title_link ): t.append(htmlescape(image_url)) elif image_url: link_shown = True for field in attachment.get("fields", []): if field.get("title"): t.append("{}: {}".format(field["title"], field["value"])) else: t.append(field["value"]) files = unwrap_files(message, attachment, None) if files: t.append(files) t = [unhtmlescape(unfurl_refs(x)) for x in t] blocks = attachment.get("blocks", []) t.extend(unfurl_blocks(blocks)) if attachment.get("is_msg_unfurl"): channel_name = resolve_ref("#{}".format(attachment["channel_id"])) if attachment.get("is_reply_unfurl"): footer = "From a thread in {}".format(channel_name) else: footer = "Posted in {}".format(channel_name) else: footer = attachment.get("footer") if footer: ts = attachment.get("ts") if ts: ts_int = ts if isinstance(ts, int) else SlackTS(ts).major if ts_int > 100000000000: # The Slack web interface interprets very large timestamps # as milliseconds after the epoch instead of regular Unix # timestamps. We use the same heuristic here. ts_int = ts_int // 1000 time_string = "" if date.today() - date.fromtimestamp(ts_int) <= timedelta(days=1): time_string = " at {time}" timestamp_formatted = resolve_ref( "!date^{}^{{date_short_pretty}}{}".format(ts_int, time_string) ).capitalize() footer += " | {}".format(timestamp_formatted) t.append(unhtmlescape(unfurl_refs(footer))) fallback = attachment.get("fallback") if t == [] and fallback and not link_shown: t.append(fallback) if t: lines = [ line for part in t for line in part.strip().split("\n") if part ] prefix = "|" line_color = None color = attachment.get("color") if color and config.colorize_attachments != "none": weechat_color = w.info_get( "color_rgb2term", str(int(color.lstrip("#"), 16)) ) if config.colorize_attachments == "prefix": prefix = colorize_string(weechat_color, prefix) elif config.colorize_attachments == "all": line_color = weechat_color attachment_texts.extend( colorize_string(line_color, "{} {}".format(prefix, line)) for line in lines ) return "\n".join(attachment_texts) def unwrap_huddle(message, message_json, text_before): """ If huddle is linked to message, append huddle information and link to connect. """ huddle_texts = [] if "room" in message_json: if "name" in message_json.get("room"): room_name = message_json.get("room").get("name") if room_name != "": huddle_texts.append("Huddle name: {}".format(room_name)) for channel in message_json.get("room").get("channels"): huddle_texts.append( "https://app.slack.com/client/{team}/{channel}?open=start_huddle".format( team=message_json.get("team"), channel=channel ) ) if text_before: huddle_texts.insert(0, "") return "\n".join(huddle_texts) def unwrap_files(message, message_json, text_before): files_texts = [] for f in message_json.get("files", []): if f.get("mode", "") == "tombstone": text = colorize_string(config.color_deleted, "(This file was deleted.)") elif f.get("mode", "") == "hidden_by_limit": text = colorize_string( config.color_deleted, "(This file is hidden because the workspace has passed its storage limit.)", ) elif f.get("mimetype") == "application/vnd.slack-docs": url = "{}?origin_team={}&origin_channel={}".format( f["permalink"], message.team.identifier, message.channel.identifier ) text = "{} ({})".format(url, f["title"]) elif f.get("url_private"): if f.get("title"): text = "{} ({})".format(f["url_private"], f["title"]) else: text = f["url_private"] else: dbg("File {} has unrecognized mode {}".format(f["id"], f.get("mode")), 5) text = colorize_string( config.color_deleted, "(This file cannot be handled.)" ) files_texts.append(text) if text_before: files_texts.insert(0, "") return "\n".join(files_texts) def resolve_ref(ref): if ref in ["!channel", "!everyone", "!group", "!here"]: return ref.replace("!", "@") for team in EVENTROUTER.teams.values(): if ref.startswith("@"): user = team.users.get(ref[1:]) if user: suffix = config.external_user_suffix if user.is_external else "" return "@{}{}".format(user.name, suffix) elif ref.startswith("#"): channel = team.channels.get(ref[1:]) if channel: return channel.name elif ref.startswith("!subteam"): _, subteam_id = ref.split("^") subteam = team.subteams.get(subteam_id) if subteam: return subteam.handle elif ref.startswith("!date"): parts = ref.split("^") ref_datetime = datetime.fromtimestamp(int(parts[1])) link_suffix = " ({})".format(parts[3]) if len(parts) > 3 else "" token_to_format = { "date_num": "%Y-%m-%d", "date": "%B %d, %Y", "date_short": "%b %d, %Y", "date_long": "%A, %B %d, %Y", "time": "%H:%M", "time_secs": "%H:%M:%S", } def replace_token(match): token = match.group(1) if token.startswith("date_") and token.endswith("_pretty"): if ref_datetime.date() == date.today(): return "today" elif ref_datetime.date() == date.today() - timedelta(days=1): return "yesterday" elif ref_datetime.date() == date.today() + timedelta(days=1): return "tomorrow" else: token = token.replace("_pretty", "") if token in token_to_format: return decode_from_utf8( ref_datetime.strftime(token_to_format[token]) ) else: return match.group(0) return re.sub(r"{([^}]+)}", replace_token, parts[2]) + link_suffix # Something else, just return as-is return ref def create_user_status_string(profile): real_name = profile.get("real_name") status_emoji = replace_string_with_emoji(profile.get("status_emoji", "")) status_text = profile.get("status_text") if status_emoji or status_text: return "{} | {} {}".format(real_name, status_emoji, status_text) else: return real_name def create_reaction_string(reaction, myidentifier): if config.show_reaction_nicks: nicks = [resolve_ref("@{}".format(user)) for user in reaction["users"]] nicks_extra = ( ["and others"] if len(reaction["users"]) < reaction["count"] else [] ) users = "({})".format(", ".join(nicks + nicks_extra)) else: users = reaction["count"] reaction_string = ":{}:{}".format(reaction["name"], users) if myidentifier in reaction["users"]: return colorize_string( config.color_reaction_suffix_added_by_you, reaction_string, reset_color=config.color_reaction_suffix, ) else: return reaction_string def create_reactions_string(reactions, myidentifier): reactions_with_users = [r for r in reactions if r["count"] > 0] reactions_string = " ".join( create_reaction_string(r, myidentifier) for r in reactions_with_users ) if reactions_string: return " " + colorize_string( config.color_reaction_suffix, "[{}]".format(reactions_string) ) else: return "" def hdata_line_ts(line_pointer): data = w.hdata_pointer(hdata.line, line_pointer, "data") for i in range(w.hdata_integer(hdata.line_data, data, "tags_count")): tag = w.hdata_string(hdata.line_data, data, "{}|tags_array".format(i)) if tag.startswith("slack_ts_"): return SlackTS(tag[9:]) return None def modify_buffer_line(buffer_pointer, ts, new_text): own_lines = w.hdata_pointer(hdata.buffer, buffer_pointer, "own_lines") line_pointer = w.hdata_pointer(hdata.lines, own_lines, "last_line") # Find the last line with this ts is_last_line = True while line_pointer and hdata_line_ts(line_pointer) != ts: is_last_line = False line_pointer = w.hdata_move(hdata.line, line_pointer, -1) if not line_pointer: return w.WEECHAT_RC_OK if weechat_version >= 0x04000000: data = w.hdata_pointer(hdata.line, line_pointer, "data") w.hdata_update(hdata.line_data, data, {"message": new_text}) return w.WEECHAT_RC_OK # Find all lines for the message pointers = [] while line_pointer and hdata_line_ts(line_pointer) == ts: pointers.append(line_pointer) line_pointer = w.hdata_move(hdata.line, line_pointer, -1) pointers.reverse() if not pointers: return w.WEECHAT_RC_OK if is_last_line: lines = new_text.split("\n") extra_lines_count = len(lines) - len(pointers) if extra_lines_count > 0: line_data = w.hdata_pointer(hdata.line, pointers[0], "data") tags_count = w.hdata_integer(hdata.line_data, line_data, "tags_count") tags = [ w.hdata_string(hdata.line_data, line_data, "{}|tags_array".format(i)) for i in range(tags_count) ] tags = tags_set_notify_none(tags) tags_str = ",".join(tags) last_read_line = w.hdata_pointer(hdata.lines, own_lines, "last_read_line") should_set_unread = last_read_line == pointers[-1] # Insert new lines to match the number of lines in the message w.buffer_set(buffer_pointer, "print_hooks_enabled", "0") for _ in range(extra_lines_count): w.prnt_date_tags(buffer_pointer, ts.major, tags_str, " \t ") pointers.append(w.hdata_pointer(hdata.lines, own_lines, "last_line")) if should_set_unread: w.buffer_set(buffer_pointer, "unread", "") w.buffer_set(buffer_pointer, "print_hooks_enabled", "1") else: # Split the message into at most the number of existing lines as we can't insert new lines lines = new_text.split("\n", len(pointers) - 1) # Replace newlines to prevent garbled lines in bare display mode lines = [line.replace("\n", " | ") for line in lines] # Extend lines in case the new message is shorter than the old as we can't delete lines lines += [""] * (len(pointers) - len(lines)) for pointer, line in zip(pointers, lines): data = w.hdata_pointer(hdata.line, pointer, "data") w.hdata_update(hdata.line_data, data, {"message": line}) return w.WEECHAT_RC_OK def nick_from_profile(profile, username): if config.use_usernames: nick = username else: full_name = profile.get("real_name") or username if config.use_full_names: nick = full_name else: nick = profile.get("display_name") or full_name return nick.replace(" ", "") def format_nick(nick, previous_nick=None): if nick == previous_nick: nick = w.config_string(w.config_get("weechat.look.prefix_same_nick")) or nick nick_prefix = w.config_string(w.config_get("weechat.look.nick_prefix")) nick_prefix_color_name = w.config_string( w.config_get("weechat.color.chat_nick_prefix") ) nick_suffix = w.config_string(w.config_get("weechat.look.nick_suffix")) nick_suffix_color_name = w.config_string( w.config_get("weechat.color.chat_nick_prefix") ) return ( colorize_string(nick_prefix_color_name, nick_prefix) + nick + colorize_string(nick_suffix_color_name, nick_suffix) ) def tags_set_notify_none(tags): notify_tags = {"notify_highlight", "notify_message", "notify_private"} tags = [tag for tag in tags if tag not in notify_tags] tags += ["no_highlight", "notify_none"] return tags def tag( ts, tagset=None, user=None, self_msg=False, backlog=False, no_log=False, extra_tags=None, ): tagsets = { "team_info": ["no_highlight", "log3"], "team_message": ["irc_privmsg", "notify_message", "log1"], "dm": ["irc_privmsg", "notify_private", "log1"], "join": ["irc_join", "no_highlight", "log4"], "leave": ["irc_part", "no_highlight", "log4"], "topic": ["irc_topic", "no_highlight", "log3"], "channel": ["irc_privmsg", "notify_message", "log1"], } ts_tag = "slack_ts_{}".format(ts) slack_tag = "slack_{}".format(tagset or "default") nick_tag = ["nick_{}".format(user).replace(" ", "_")] if user else [] tags = [ts_tag, slack_tag] + nick_tag + tagsets.get(tagset, []) if (self_msg and tagset != "join") or backlog: tags = tags_set_notify_none(tags) if self_msg: tags += ["self_msg"] if backlog: tags += ["logger_backlog"] if no_log: tags += ["no_log"] tags = [ tag for tag in tags if not tag.startswith("log") or tag == "logger_backlog" ] if extra_tags: tags += extra_tags return ",".join(OrderedDict.fromkeys(tags)) def set_own_presence_active(team): if config.use_usernames: nick_slackbot = "slackbot" else: nick_slackbot = "Slackbot" slackbot = team.get_channel_map()[nick_slackbot] channel = team.channels[slackbot] request = {"type": "typing", "channel": channel.identifier} channel.team.send_to_websocket(request, expect_reply=False) ###### New/converted command_ commands @slack_buffer_or_ignore @utf8_decode def invite_command_cb(data, current_buffer, args): team = EVENTROUTER.weechat_controller.buffers[current_buffer].team split_args = args.split()[1:] if not split_args: w.prnt( "", 'Too few arguments for command "/invite" (help on command: /help invite)', ) return w.WEECHAT_RC_OK_EAT if split_args[-1].startswith("#") or split_args[-1].startswith( config.group_name_prefix ): nicks = split_args[:-1] channel = team.channels.get(team.get_channel_map().get(split_args[-1])) if not nicks or not channel: w.prnt("", "{}: No such nick/channel".format(split_args[-1])) return w.WEECHAT_RC_OK_EAT else: nicks = split_args channel = EVENTROUTER.weechat_controller.buffers[current_buffer] all_users = team.get_username_map() users = set() for nick in nicks: user = all_users.get(nick.lstrip("@")) if not user: w.prnt("", "ERROR: Unknown user: {}".format(nick)) return w.WEECHAT_RC_OK_EAT users.add(user) s = SlackRequest( team, "conversations.invite", {"channel": channel.identifier, "users": ",".join(users)}, channel=channel, metadata={"nicks": nicks}, ) EVENTROUTER.receive(s) return w.WEECHAT_RC_OK_EAT @slack_buffer_or_ignore @utf8_decode def part_command_cb(data, current_buffer, args): e = EVENTROUTER args = args.split() if len(args) > 1: team = e.weechat_controller.buffers[current_buffer].team cmap = team.get_channel_map() channel = "".join(args[1:]) if channel in cmap: buffer_ptr = team.channels[cmap[channel]].channel_buffer e.weechat_controller.unregister_buffer( buffer_ptr, update_remote=True, close_buffer=True ) else: w.prnt(team.channel_buffer, "{}: No such channel".format(channel)) else: e.weechat_controller.unregister_buffer( current_buffer, update_remote=True, close_buffer=True ) return w.WEECHAT_RC_OK_EAT def parse_topic_command(command): _, _, args = command.partition(" ") if args.startswith("#"): channel_name, _, topic_arg = args.partition(" ") else: channel_name = None topic_arg = args if topic_arg == "-delete": topic = "" elif topic_arg: topic = topic_arg else: topic = None return channel_name, topic @slack_buffer_or_ignore @utf8_decode def topic_command_cb(data, current_buffer, command): """ Change the topic of a channel /topic [] [|-delete] """ channel_name, topic = parse_topic_command(command) team = EVENTROUTER.weechat_controller.buffers[current_buffer].team if channel_name: channel = team.channels.get(team.get_channel_map().get(channel_name)) else: channel = EVENTROUTER.weechat_controller.buffers[current_buffer] if not channel: w.prnt(team.channel_buffer, "{}: No such channel".format(channel_name)) return w.WEECHAT_RC_OK_EAT if topic is None: w.prnt( channel.channel_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.render_topic()), ) else: s = SlackRequest( team, "conversations.setTopic", {"channel": channel.identifier, "topic": linkify_text(topic, team)}, channel=channel, ) EVENTROUTER.receive(s) return w.WEECHAT_RC_OK_EAT @slack_buffer_or_ignore @utf8_decode def whois_command_cb(data, current_buffer, command): """ Get real name of user /whois """ args = command.split() if len(args) < 2: w.prnt(current_buffer, "Not enough arguments") return w.WEECHAT_RC_OK_EAT user = args[1] if user.startswith("@"): user = user[1:] team = EVENTROUTER.weechat_controller.buffers[current_buffer].team u = team.users.get(team.get_username_map().get(user)) if u: def print_profile(field): value = u.profile.get(field) if value: team.buffer_prnt("[{}]: {}: {}".format(user, field, value)) team.buffer_prnt("[{}]: {}".format(user, u.real_name)) status_emoji = replace_string_with_emoji(u.profile.get("status_emoji", "")) status_text = u.profile.get("status_text", "") if status_emoji or status_text: team.buffer_prnt("[{}]: {} {}".format(user, status_emoji, status_text)) team.buffer_prnt("[{}]: username: {}".format(user, u.username)) team.buffer_prnt("[{}]: id: {}".format(user, u.identifier)) print_profile("title") print_profile("email") print_profile("phone") print_profile("skype") else: team.buffer_prnt("[{}]: No such user".format(user)) return w.WEECHAT_RC_OK_EAT @slack_buffer_or_ignore @utf8_decode def me_command_cb(data, current_buffer, args): channel = EVENTROUTER.weechat_controller.buffers[current_buffer] message = args.split(" ", 1)[1] channel.send_message(message, subtype="me_message") return w.WEECHAT_RC_OK_EAT @utf8_decode def command_register(data, current_buffer, args): """ /slack register [-nothirdparty] [code/token] Register a Slack team in wee-slack. Call this without any arguments and follow the instructions to register a new team. If you already have a token for a team, you can call this with that token to add it. By default GitHub Pages will see a temporary code used to create your token (but not the token itself). If you're worried about this, you can use the -nothirdparty option, though the process will be a bit less user friendly. """ CLIENT_ID = "2468770254.51917335286" CLIENT_SECRET = "dcb7fe380a000cba0cca3169a5fe8d70" # Not really a secret. REDIRECT_URI_GITHUB = "https://wee-slack.github.io/wee-slack/oauth" REDIRECT_URI_NOTHIRDPARTY = "http://not.a.realhost/" args = args.strip() if " " in args: nothirdparty_arg, _, code = args.partition(" ") nothirdparty = nothirdparty_arg == "-nothirdparty" else: nothirdparty = args == "-nothirdparty" code = "" if nothirdparty else args redirect_uri = quote( REDIRECT_URI_NOTHIRDPARTY if nothirdparty else REDIRECT_URI_GITHUB, safe="" ) if not code: if nothirdparty: nothirdparty_note = "" last_step = "You will see a message that the site can't be reached, this is expected. The URL for the page will have a code in it of the form `?code=`. Copy the code after the equals sign, return to WeeChat and run `/slack register -nothirdparty `." else: nothirdparty_note = "\nNote that by default GitHub Pages will see a temporary code used to create your token (but not the token itself). If you're worried about this, you can use the -nothirdparty option, though the process will be a bit less user friendly." last_step = "The web page will show a command in the form `/slack register `. Run this command in WeeChat." message = ( textwrap.dedent( """ ### Connecting to a Slack team with OAuth ###{} 1) Paste this link into a browser: https://slack.com/oauth/authorize?client_id={}&scope=client&redirect_uri={} 2) Select the team you wish to access from wee-slack in your browser. If you want to add multiple teams, you will have to repeat this whole process for each team. 3) Click "Authorize" in the browser. If you get a message saying you are not authorized to install wee-slack, the team has restricted Slack app installation and you will have to request it from an admin. To do that, go to https://my.slack.com/apps/A1HSZ9V8E-wee-slack and click "Request to Install". 4) {} """ ) .strip() .format(nothirdparty_note, CLIENT_ID, redirect_uri, last_step) ) w.prnt("", "\n" + message) return w.WEECHAT_RC_OK_EAT elif code.startswith("xox"): add_token(code) return w.WEECHAT_RC_OK_EAT uri = ( "https://slack.com/api/oauth.access?" "client_id={}&client_secret={}&redirect_uri={}&code={}" ).format(CLIENT_ID, CLIENT_SECRET, redirect_uri, code) params = {"useragent": "wee_slack {}".format(SCRIPT_VERSION)} w.hook_process_hashtable( "url:{}".format(uri), params, config.slack_timeout, "register_callback", "" ) return w.WEECHAT_RC_OK_EAT command_register.completion = "-nothirdparty %-" @utf8_decode def register_callback(data, command, return_code, out, err): if return_code != 0: w.prnt( "", "ERROR: problem when trying to get Slack OAuth token. Got return code {}. Err: {}".format( return_code, err ), ) w.prnt("", "Check the network or proxy settings") return w.WEECHAT_RC_OK_EAT if len(out) <= 0: w.prnt( "", "ERROR: problem when trying to get Slack OAuth token. Got 0 length answer. Err: {}".format( err ), ) w.prnt("", "Check the network or proxy settings") return w.WEECHAT_RC_OK_EAT d = json.loads(out) if not d["ok"]: w.prnt("", "ERROR: Couldn't get Slack OAuth token: {}".format(d["error"])) return w.WEECHAT_RC_OK_EAT add_token(d["access_token"], d["team_name"]) return w.WEECHAT_RC_OK_EAT def add_token(token, team_name=None): if config.is_default("slack_api_token"): w.config_set_plugin("slack_api_token", token) else: # Add new token to existing set, joined by comma. existing_tokens = config.get_string("slack_api_token") if token in existing_tokens: print_error("This token is already registered") return w.config_set_plugin("slack_api_token", ",".join([existing_tokens, token])) if team_name: w.prnt("", 'Success! Added team "{}"'.format(team_name)) else: w.prnt("", "Success! Added token") w.prnt("", "Please reload wee-slack with: /python reload slack") w.prnt( "", "If you want to add another team you can repeat this process from step 1 before reloading wee-slack.", ) @slack_buffer_or_ignore @utf8_decode def msg_command_cb(data, current_buffer, args): aargs = args.split(None, 2) who = aargs[1].lstrip("@") if who != "*": join_query_command_cb(data, current_buffer, "/query " + who) if len(aargs) > 2: message = aargs[2] buffer_pointer = EVENTROUTER.weechat_controller.buffers[current_buffer] team = buffer_pointer.team if who == "*": channel = buffer_pointer else: cmap = team.get_channel_map() channel = team.channels.get(cmap.get(who)) if channel: channel.send_message(message) return w.WEECHAT_RC_OK_EAT def print_team_items_info(team, header, items, extra_info_function): team.buffer_prnt("{}:".format(header)) if items: max_name_length = max(len(item.name) for item in items) for item in sorted(items, key=lambda item: item.name.lower()): extra_info = extra_info_function(item) team.buffer_prnt( " {:<{}}({})".format(item.name, max_name_length + 2, extra_info) ) return w.WEECHAT_RC_OK_EAT def print_users_info(team, header, users): def extra_info_function(user): external_text = ", external" if user.is_external else "" return user.presence + external_text return print_team_items_info(team, header, users, extra_info_function) @slack_buffer_required @utf8_decode def command_teams(data, current_buffer, args): """ /slack teams List the connected Slack teams. """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team teams = EVENTROUTER.teams.values() extra_info_function = lambda team: "token: {}".format(token_for_print(team.token)) return print_team_items_info(team, "Slack teams", teams, extra_info_function) @slack_buffer_required @utf8_decode def command_channels(data, current_buffer, args): """ /slack channels [regex] List the channels in the current team. If regex is given show channels whose names match the regular expression. """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team pat = re.compile(args) channels = [ channel for channel in team.channels.values() if channel.type not in ["im", "mpim"] and pat.search(channel.name) ] def extra_info_function(channel): if channel.active: return "member" elif getattr(channel, "is_archived", None): return "archived" else: return "not a member" if args: return print_team_items_info( team, 'Channels that match "' + args + '"', channels, extra_info_function ) else: return print_team_items_info(team, "Channels", channels, extra_info_function) @slack_buffer_required @utf8_decode def command_users(data, current_buffer, args): """ /slack users [regex] List the users in the current team. If regex is given show only users that match the case-insensitive regex. """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team if args: pat = re.compile(args, flags=re.IGNORECASE) users = [v for v in team.users.values() if pat.search(v.name)] header = 'Users that match "{}"'.format(args) else: users = team.users.values() header = "Users" return print_users_info(team, header, users) @slack_buffer_required @utf8_decode def command_usergroups(data, current_buffer, args): """ /slack usergroups [handle] List the usergroups in the current team If handle is given show the members in the usergroup """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team usergroups = team.generate_usergroup_map() usergroup_key = usergroups.get(args) if usergroup_key: s = SlackRequest( team, "usergroups.users.list", {"usergroup": usergroup_key}, metadata={"usergroup_handle": args}, ) EVENTROUTER.receive(s) elif args: w.prnt("", "ERROR: Unknown usergroup handle: {}".format(args)) return w.WEECHAT_RC_ERROR else: def extra_info_function(subteam): is_member = "member" if subteam.is_member else "not a member" return "{}, {}".format(subteam.handle, is_member) return print_team_items_info( team, "Usergroups", team.subteams.values(), extra_info_function ) return w.WEECHAT_RC_OK_EAT command_usergroups.completion = "%(usergroups) %-" @slack_buffer_required @utf8_decode def command_talk(data, current_buffer, args): """ /slack talk [,[,...]] Open a chat with the specified user(s). """ if not args: w.prnt("", "Usage: /slack talk [,[,...]]") return w.WEECHAT_RC_ERROR return join_query_command_cb(data, current_buffer, "/query " + args) command_talk.completion = "%(nicks)" @slack_buffer_or_ignore @utf8_decode def join_query_command_cb(data, current_buffer, args): team = EVENTROUTER.weechat_controller.buffers[current_buffer].team split_args = args.split(" ", 1) if len(split_args) < 2 or not split_args[1]: w.prnt( "", 'Too few arguments for command "{}" (help on command: /help {})'.format( split_args[0], split_args[0].lstrip("/") ), ) return w.WEECHAT_RC_OK_EAT query = split_args[1] # Try finding the channel by name channel = team.channels.get(team.get_channel_map().get(query)) # If the channel doesn't exist, try finding a DM or MPDM instead if not channel: if query.startswith("#"): w.prnt("", "ERROR: Unknown channel: {}".format(query)) return w.WEECHAT_RC_OK_EAT # Get the IDs of the users all_users = team.get_username_map() users = set() for username in query.split(","): user = all_users.get(username.lstrip("@")) if not user: w.prnt("", "ERROR: Unknown user: {}".format(username)) return w.WEECHAT_RC_OK_EAT users.add(user) if users: if len(users) > 1: channel_type = "mpim" # Add the current user since MPDMs include them as a member users.add(team.myidentifier) else: channel_type = "im" channel = team.find_channel_by_members(users, channel_type=channel_type) # If the DM or MPDM doesn't exist, create it if not channel: s = SlackRequest( team, team.slack_api_translator[channel_type]["join"], {"users": ",".join(users)}, metadata={"switch": True}, ) EVENTROUTER.receive(s) if channel: channel.open() if config.switch_buffer_on_join: w.buffer_set(channel.channel_buffer, "display", "1") return w.WEECHAT_RC_OK_EAT @slack_buffer_required @utf8_decode def command_create(data, current_buffer, args): """ /slack create [-private] Create a public or private channel. """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team parts = args.split(None, 1) if parts[0] == "-private": args = parts[1] private = True else: private = False post_data = {"name": args, "is_private": private} s = SlackRequest(team, "conversations.create", post_data) EVENTROUTER.receive(s) return w.WEECHAT_RC_OK_EAT command_create.completion = "-private" @slack_buffer_required @utf8_decode def command_showmuted(data, current_buffer, args): """ /slack showmuted List the muted channels in the current team. """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team muted_channels = [ team.channels[key].name for key in team.muted_channels if key in team.channels ] team.buffer_prnt("Muted channels: {}".format(", ".join(muted_channels))) return w.WEECHAT_RC_OK_EAT @slack_buffer_required @utf8_decode def command_thread(data, current_buffer, args): """ /thread [count/message_id] Open the thread for the message. If no message id is specified the last thread in channel will be opened. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] if not isinstance(channel, SlackChannelCommon): print_error("/thread can not be used in the team buffer, only in a channel") return w.WEECHAT_RC_ERROR message = channel.message_from_hash(args) if not message: message_filter = lambda message: message.number_of_replies() message = channel.message_from_hash_or_index(args, message_filter) if message: message.open_thread(switch=config.switch_buffer_on_join) elif args: print_error( "Invalid id given, must be an existing id or a number greater " + "than 0 and less than the number of thread messages in the channel" ) else: print_error("No threads found in channel") return w.WEECHAT_RC_OK_EAT command_thread.completion = "%(threads) %-" def subscribe_helper(current_buffer, args, usage, api): channel = EVENTROUTER.weechat_controller.buffers[current_buffer] team = channel.team if isinstance(channel, SlackThreadChannel) and not args: message = channel.parent_message else: message_filter = lambda message: message.number_of_replies() message = channel.message_from_hash_or_index(args, message_filter) if not message: print_message_not_found_error(args) return w.WEECHAT_RC_OK_EAT last_read = next(reversed(message.submessages), message.ts) post_data = { "channel": channel.identifier, "thread_ts": message.ts, "last_read": last_read, } s = SlackRequest(team, api, post_data, channel=channel) EVENTROUTER.receive(s) return w.WEECHAT_RC_OK_EAT @slack_buffer_required @utf8_decode def command_subscribe(data, current_buffer, args): """ /slack subscribe Subscribe to a thread, so that you are alerted to new messages. When in a thread buffer, you can omit the thread id. This command only works when using a session token, see the readme: https://github.com/wee-slack/wee-slack#4-add-your-slack-api-tokens """ return subscribe_helper( current_buffer, args, "Usage: /slack subscribe ", "subscriptions.thread.add", ) command_subscribe.completion = "%(threads) %-" @slack_buffer_required @utf8_decode def command_unsubscribe(data, current_buffer, args): """ /slack unsubscribe Unsubscribe from a thread that has been previously subscribed to, so that you are not alerted to new messages. When in a thread buffer, you can omit the thread id. This command only works when using a session token, see the readme: https://github.com/wee-slack/wee-slack#4-add-your-slack-api-tokens """ return subscribe_helper( current_buffer, args, "Usage: /slack unsubscribe ", "subscriptions.thread.remove", ) command_unsubscribe.completion = "%(threads) %-" @slack_buffer_required @utf8_decode def command_reply(data, current_buffer, args): """ /reply [-alsochannel] [] When in a channel buffer: /reply [-alsochannel] Reply in a thread on the message. Specify either the message id or a count upwards to the message from the last message. When in a thread buffer: /reply [-alsochannel] Reply to the current thread. This can be used to send the reply to the rest of the channel. In either case, -alsochannel also sends the reply to the parent channel. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] parts = args.split(None, 1) if len(parts) < 1: w.prnt( "", 'Too few arguments for command "/reply" (help on command: /help reply)' ) return w.WEECHAT_RC_ERROR if parts[0] == "-alsochannel": args = parts[1] broadcast = True else: broadcast = False if isinstance(channel, SlackThreadChannel): text = args message = channel.parent_message else: try: msg_id, text = args.split(None, 1) except ValueError: w.prnt( "", "Usage (when in a channel buffer): /reply [-alsochannel] ", ) return w.WEECHAT_RC_OK_EAT message = channel.message_from_hash_or_index(msg_id) if not message: print_message_not_found_error(args) return w.WEECHAT_RC_OK_EAT if isinstance(message, SlackThreadMessage): parent_id = str(message.parent_message.ts) elif message: parent_id = str(message.ts) channel.send_message( text, request_dict_ext={"thread_ts": parent_id, "reply_broadcast": broadcast} ) return w.WEECHAT_RC_OK_EAT command_reply.completion = "%(threads)|-alsochannel %(threads)" @slack_buffer_required @utf8_decode def command_rehistory(data, current_buffer, args): """ /rehistory [-remote] Reload the history in the current channel. With -remote the history will be downloaded again from Slack. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] if args == "-remote": channel.get_history(full=True, no_log=True) else: channel.reprint_messages(force_render=True) return w.WEECHAT_RC_OK_EAT command_rehistory.completion = "-remote" @slack_buffer_required @utf8_decode def command_hide(data, current_buffer, args): """ /hide Hide the current channel if it is marked as distracting. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] name = channel.formatted_name(style="long_default") if name in config.distracting_channels: w.buffer_set(channel.channel_buffer, "hidden", "1") return w.WEECHAT_RC_OK_EAT @utf8_decode def slack_command_cb(data, current_buffer, args): split_args = args.split(" ", 1) cmd_name = split_args[0] cmd_args = split_args[1] if len(split_args) > 1 else "" cmd = EVENTROUTER.cmds.get(cmd_name or "help") if not cmd: w.prnt("", "Command not found: " + cmd_name) return w.WEECHAT_RC_OK return cmd(data, current_buffer, cmd_args) @utf8_decode def command_help(data, current_buffer, args): """ /slack help [command] Print help for /slack commands. """ if args: cmd = EVENTROUTER.cmds.get(args) if cmd: cmds = {args: cmd} else: w.prnt("", "Command not found: " + args) return w.WEECHAT_RC_OK else: cmds = EVENTROUTER.cmds w.prnt("", "\n{}".format(colorize_string("bold", "Slack commands:"))) script_prefix = "{0}[{1}python{0}/{1}slack{0}]{1}".format( w.color("green"), w.color("reset") ) for _, cmd in sorted(cmds.items()): name, cmd_args, description = parse_help_docstring(cmd) w.prnt( "", "\n{} {} {}\n\n{}".format( script_prefix, colorize_string("white", name), cmd_args, description ), ) return w.WEECHAT_RC_OK @slack_buffer_required @utf8_decode def command_distracting(data, current_buffer, args): """ /slack distracting Add or remove the current channel from distracting channels. You can hide or unhide these channels with /slack nodistractions. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] fullname = channel.formatted_name(style="long_default") if fullname in config.distracting_channels: config.distracting_channels.remove(fullname) else: config.distracting_channels.append(fullname) w.config_set_plugin("distracting_channels", ",".join(config.distracting_channels)) return w.WEECHAT_RC_OK_EAT @slack_buffer_required @utf8_decode def command_slash(data, current_buffer, args): """ /slack slash /customcommand arg1 arg2 arg3 Run a custom slack command. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] team = channel.team split_args = args.split(" ", 1) command = split_args[0] text = split_args[1] if len(split_args) > 1 else "" text_linkified = linkify_text(text, team, only_users=True) s = SlackRequest( team, "chat.command", {"command": command, "text": text_linkified, "channel": channel.identifier}, channel=channel, metadata={"command": command, "command_args": text}, ) EVENTROUTER.receive(s) return w.WEECHAT_RC_OK_EAT @slack_buffer_required @utf8_decode def command_mute(data, current_buffer, args): """ /slack mute Toggle mute on the current channel. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] team = channel.team team.muted_channels ^= {channel.identifier} muted_str = "Muted" if channel.identifier in team.muted_channels else "Unmuted" team.buffer_prnt("{} channel {}".format(muted_str, channel.name)) s = SlackRequest( team, "users.prefs.set", {"name": "muted_channels", "value": ",".join(team.muted_channels)}, channel=channel, ) EVENTROUTER.receive(s) return w.WEECHAT_RC_OK_EAT @slack_buffer_required @utf8_decode def command_linkarchive(data, current_buffer, args): """ /slack linkarchive [message_id] Place a link to the channel or message in the input bar. Use cursor or mouse mode to get the id. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] url = "https://{}/".format(channel.team.domain) if isinstance(channel, SlackChannelCommon): url += "archives/{}/".format(channel.identifier) if args: message = channel.message_from_hash_or_index(args) if message: url += "p{}{:0>6}".format(message.ts.majorstr(), message.ts.minorstr()) if isinstance(message, SlackThreadMessage): url += "?thread_ts={}&cid={}".format( message.parent_message.ts, channel.identifier ) else: print_message_not_found_error(args) return w.WEECHAT_RC_OK_EAT w.command(current_buffer, "/input insert {}".format(url)) return w.WEECHAT_RC_OK_EAT command_linkarchive.completion = "%(threads) %-" @utf8_decode def command_nodistractions(data, current_buffer, args): """ /slack nodistractions Hide or unhide all channels marked as distracting. """ global hide_distractions hide_distractions = not hide_distractions channels = [ channel for channel in EVENTROUTER.weechat_controller.buffers.values() if channel in config.distracting_channels ] for channel in channels: w.buffer_set(channel.channel_buffer, "hidden", str(int(hide_distractions))) return w.WEECHAT_RC_OK_EAT @slack_buffer_required @utf8_decode def command_upload(data, current_buffer, args): """ /slack upload Uploads a file to the current buffer. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] weechat_dir = w.info_get("weechat_data_dir", "") or w.info_get("weechat_dir", "") file_path = os.path.join(weechat_dir, os.path.expanduser(args)) if channel.type == "team": w.prnt("", "ERROR: Can't upload a file to the team buffer") return w.WEECHAT_RC_ERROR if not os.path.isfile(file_path): unescaped_file_path = file_path.replace(r"\ ", " ") if os.path.isfile(unescaped_file_path): file_path = unescaped_file_path else: w.prnt("", "ERROR: Could not find file: {}".format(file_path)) return w.WEECHAT_RC_ERROR post_data = { "channels": channel.identifier, } if isinstance(channel, SlackThreadChannel): post_data["thread_ts"] = channel.thread_ts request = SlackRequest(channel.team, "files.upload", post_data, channel=channel) options = request.options_as_cli_args() + [ "-s", "-Ffile=@{}".format(file_path), request.request_string(), ] proxy_string = ProxyWrapper().curl() if proxy_string: options.append(proxy_string) options_hashtable = {"arg{}".format(i + 1): arg for i, arg in enumerate(options)} w.hook_process_hashtable( "curl", options_hashtable, config.slack_timeout, "upload_callback", "" ) return w.WEECHAT_RC_OK_EAT command_upload.completion = "%(filename) %-" @utf8_decode def upload_callback(data, command, return_code, out, err): if return_code != 0: w.prnt( "", "ERROR: Couldn't upload file. Got return code {}. Error: {}".format( return_code, err ), ) return w.WEECHAT_RC_OK_EAT try: response = json.loads(out) except JSONDecodeError: w.prnt( "", "ERROR: Couldn't process response from file upload. Got: {}".format(out) ) return w.WEECHAT_RC_OK_EAT if not response["ok"]: w.prnt("", "ERROR: Couldn't upload file. Error: {}".format(response["error"])) return w.WEECHAT_RC_OK_EAT @utf8_decode def away_command_cb(data, current_buffer, args): all_servers, message = re.match("^/away( -all)? ?(.*)", args).groups() if all_servers: team_buffers = [team.channel_buffer for team in EVENTROUTER.teams.values()] elif current_buffer in EVENTROUTER.weechat_controller.buffers: team_buffers = [current_buffer] else: return w.WEECHAT_RC_OK for team_buffer in team_buffers: if message: command_away(data, team_buffer, args) else: command_back(data, team_buffer, args) return w.WEECHAT_RC_OK @slack_buffer_required @utf8_decode def command_away(data, current_buffer, args): """ /slack away Sets your status as 'away'. """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team s = SlackRequest(team, "users.setPresence", {"presence": "away"}) EVENTROUTER.receive(s) return w.WEECHAT_RC_OK @slack_buffer_required @utf8_decode def command_status(data, current_buffer, args): """ /slack status [ []|-delete] Lets you set your Slack Status (not to be confused with away/here). Prints current status if no arguments are given, unsets the status if -delete is given. """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team split_args = args.split(" ", 1) if not split_args[0]: profile = team.users[team.myidentifier].profile team.buffer_prnt( "Status: {} {}".format( replace_string_with_emoji(profile.get("status_emoji", "")), profile.get("status_text", ""), ) ) return w.WEECHAT_RC_OK emoji = "" if split_args[0] == "-delete" else split_args[0] text = split_args[1] if len(split_args) > 1 else "" new_profile = {"status_text": text, "status_emoji": emoji} s = SlackRequest(team, "users.profile.set", {"profile": new_profile}) EVENTROUTER.receive(s) return w.WEECHAT_RC_OK command_status.completion = "-delete|%(emoji) %-" @utf8_decode def line_event_cb(data, signal, hashtable): tags = hashtable["_chat_line_tags"].split(",") for tag in tags: if tag.startswith("slack_ts_"): ts = SlackTS(tag[9:]) break else: return w.WEECHAT_RC_OK buffer_pointer = hashtable["_buffer"] channel = EVENTROUTER.weechat_controller.buffers.get(buffer_pointer) if isinstance(channel, SlackChannelCommon): message_hash = channel.hashed_messages[ts] if message_hash is None: return w.WEECHAT_RC_OK message_hash = "$" + message_hash if data == "auto": reaction = EMOJI_CHAR_OR_NAME_REGEX.match(hashtable["_chat_eol"]) if reaction: emoji = reaction.group("emoji_char") or reaction.group("emoji_name") channel.send_change_reaction("toggle", message_hash, emoji) else: data = "message" if data == "message": w.command(buffer_pointer, "/cursor stop") w.command(buffer_pointer, "/input insert {}".format(message_hash)) elif data == "delete": w.command(buffer_pointer, "/input send {}s///".format(message_hash)) elif data == "linkarchive": w.command(buffer_pointer, "/cursor stop") w.command(buffer_pointer, "/slack linkarchive {}".format(message_hash)) elif data == "reply": w.command(buffer_pointer, "/cursor stop") w.command( buffer_pointer, "/input insert /reply {}\\x20".format(message_hash) ) elif data == "thread": w.command(buffer_pointer, "/cursor stop") w.command(buffer_pointer, "/thread {}".format(message_hash)) return w.WEECHAT_RC_OK @utf8_decode def info_slack_message_cb(data, info_name, args): current_channel = EVENTROUTER.weechat_controller.buffers.get(w.current_buffer()) message = current_channel.message_from_hash_or_index(args) if not message: print_message_not_found_error(args) return "" return message.render() @slack_buffer_required @utf8_decode def command_back(data, current_buffer, args): """ /slack back Sets your status as 'back'. """ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team s = SlackRequest(team, "users.setPresence", {"presence": "auto"}) EVENTROUTER.receive(s) set_own_presence_active(team) return w.WEECHAT_RC_OK @slack_buffer_required @utf8_decode def command_label(data, current_buffer, args): """ /label [-full] |-unset Rename a channel or thread buffer. Note that this is not permanent, it will only last as long as you keep the buffer and wee-slack open. Changes the short_name by default, and the name and full_name if you use the -full option. If you haven't set the short_name explicitly, that will also be changed when using the -full option. Use the -unset option to set it back to the default. """ channel = EVENTROUTER.weechat_controller.buffers[current_buffer] split_args = args.split(None, 1) if split_args[0] == "-full": channel.label_full_drop_prefix = False channel.label_full = split_args[1] if split_args[1] != "-unset" else None else: channel.label_short_drop_prefix = False channel.label_short = args if args != "-unset" else None channel.rename() return w.WEECHAT_RC_OK command_label.completion = "-unset|-full -unset %-" @utf8_decode def set_unread_cb(data, current_buffer, command): for channel in EVENTROUTER.weechat_controller.buffers.values(): channel.mark_read() return w.WEECHAT_RC_OK @slack_buffer_or_ignore @utf8_decode def set_unread_current_buffer_cb(data, current_buffer, command): channel = EVENTROUTER.weechat_controller.buffers[current_buffer] channel.mark_read() return w.WEECHAT_RC_OK ###### NEW EXCEPTIONS class InvalidType(Exception): """ Raised when we do type checking to ensure objects of the wrong type are not used improperly. """ def __init__(self, type_str): super(InvalidType, self).__init__(type_str) ###### New but probably old and need to migrate def closed_slack_debug_buffer_cb(data, buffer): global slack_debug slack_debug = None return w.WEECHAT_RC_OK def create_slack_debug_buffer(): global slack_debug, debug_string if slack_debug is None: debug_string = None slack_debug = w.buffer_new( "slack-debug", "", "", "closed_slack_debug_buffer_cb", "" ) w.buffer_set(slack_debug, "print_hooks_enabled", "0") w.buffer_set(slack_debug, "notify", "0") w.buffer_set(slack_debug, "highlight_tags_restrict", "highlight_force") def load_emoji(): try: weechat_dir = w.info_get("weechat_data_dir", "") or w.info_get( "weechat_dir", "" ) weechat_sharedir = w.info_get("weechat_sharedir", "") local_weemoji, global_weemoji = ( "{}/weemoji.json".format(path) for path in (weechat_dir, weechat_sharedir) ) path = ( global_weemoji if os.path.exists(global_weemoji) and not os.path.exists(local_weemoji) else local_weemoji ) with open(path, "r") as ef: emojis = json.loads(ef.read()) if "emoji" in emojis: print_error( "The weemoji.json file is in an old format. Please update it." ) else: emoji_unicode = {key: value["unicode"] for key, value in emojis.items()} emoji_skin_tones = { skin_tone["name"]: skin_tone["unicode"] for emoji in emojis.values() for skin_tone in emoji.get("skinVariations", {}).values() } emoji_with_skin_tones = chain( emoji_unicode.items(), emoji_skin_tones.items() ) emoji_with_skin_tones_reverse = {v: k for k, v in emoji_with_skin_tones} return emoji_unicode, emoji_with_skin_tones_reverse except: dbg("Couldn't load emoji list: {}".format(format_exc_only()), 5) return {}, {} def parse_help_docstring(cmd): doc = textwrap.dedent(cmd.__doc__).strip().split("\n", 1) cmd_line = doc[0].split(None, 1) args = "".join(cmd_line[1:]) return cmd_line[0], args, doc[1].strip() def setup_hooks(): w.bar_item_new("slack_typing_notice", "(extra)typing_bar_item_cb", "") w.bar_item_new("away", "(extra)away_bar_item_cb", "") w.bar_item_new("slack_away", "(extra)away_bar_item_cb", "") w.hook_timer(5000, 0, 0, "ws_ping_cb", "") w.hook_timer(1000, 0, 0, "typing_update_cb", "") w.hook_timer(1000, 0, 0, "buffer_list_update_callback", "") w.hook_timer(3000, 0, 0, "reconnect_callback", "EVENTROUTER") w.hook_timer(1000 * 60 * 5, 0, 0, "slack_never_away_cb", "") w.hook_signal("buffer_closing", "buffer_closing_callback", "") w.hook_signal("buffer_renamed", "buffer_renamed_cb", "") w.hook_signal("buffer_switch", "buffer_switch_callback", "") w.hook_signal("window_switch", "buffer_switch_callback", "") w.hook_signal("quit", "quit_notification_callback", "") if config.send_typing_notice: w.hook_signal("input_text_changed", "typing_notification_cb", "") command_help.completion = "|".join(EVENTROUTER.cmds.keys()) completions = "||".join( "{} {}".format(name, getattr(cmd, "completion", "")) for name, cmd in EVENTROUTER.cmds.items() ) w.hook_command( # Command name and description "slack", "Plugin to allow typing notification and sync of read markers for slack.com", # Usage " []", # Description of arguments "Commands:\n" + "\n".join(sorted(EVENTROUTER.cmds.keys())) + "\nUse /slack help to find out more\n", # Completions completions, # Function name "slack_command_cb", "", ) w.hook_command_run("/me", "me_command_cb", "") w.hook_command_run("/query", "join_query_command_cb", "") w.hook_command_run("/join", "join_query_command_cb", "") w.hook_command_run("/part", "part_command_cb", "") w.hook_command_run("/topic", "topic_command_cb", "") w.hook_command_run("/msg", "msg_command_cb", "") w.hook_command_run("/invite", "invite_command_cb", "") w.hook_command_run("/input complete_next", "complete_next_cb", "") w.hook_command_run("/input set_unread", "set_unread_cb", "") w.hook_command_run( "/input set_unread_current_buffer", "set_unread_current_buffer_cb", "" ) w.hook_command_run("/buffer set unread", "set_unread_current_buffer_cb", "") w.hook_command_run("/away", "away_command_cb", "") w.hook_command_run("/whois", "whois_command_cb", "") for cmd_name in ["hide", "label", "rehistory", "reply", "thread"]: cmd = EVENTROUTER.cmds[cmd_name] _, args, description = parse_help_docstring(cmd) completion = getattr(cmd, "completion", "") w.hook_command( cmd_name, description, args, "", completion, "command_" + cmd_name, "" ) w.hook_completion( "irc_channel_topic", "complete topic for slack", "topic_completion_cb", "" ) w.hook_completion( "irc_channels", "complete channels for slack", "channel_completion_cb", "" ) w.hook_completion( "irc_privates", "complete dms/mpdms for slack", "dm_completion_cb", "" ) w.hook_completion("nicks", "complete @-nicks for slack", "nick_completion_cb", "") w.hook_completion( "threads", "complete thread ids for slack", "thread_completion_cb", "" ) w.hook_completion( "usergroups", "complete @-usergroups for slack", "usergroups_completion_cb", "" ) w.hook_completion("emoji", "complete :emoji: for slack", "emoji_completion_cb", "") w.key_bind( "mouse", { "@chat(python.*):button2": "hsignal:slack_mouse", }, ) w.key_bind( "cursor", { "@chat(python.*):D": "hsignal:slack_cursor_delete", "@chat(python.*):L": "hsignal:slack_cursor_linkarchive", "@chat(python.*):M": "hsignal:slack_cursor_message", "@chat(python.*):R": "hsignal:slack_cursor_reply", "@chat(python.*):T": "hsignal:slack_cursor_thread", }, ) w.hook_hsignal("slack_mouse", "line_event_cb", "auto") w.hook_hsignal("slack_cursor_delete", "line_event_cb", "delete") w.hook_hsignal("slack_cursor_linkarchive", "line_event_cb", "linkarchive") w.hook_hsignal("slack_cursor_message", "line_event_cb", "message") w.hook_hsignal("slack_cursor_reply", "line_event_cb", "reply") w.hook_hsignal("slack_cursor_thread", "line_event_cb", "thread") w.hook_info( "slack_message", "get contents of a slack message", "id or count to the message", "info_slack_message_cb", "", ) # Hooks to fix/implement # w.hook_signal('buffer_opened', "buffer_opened_cb", "") # w.hook_signal('window_scrolled', "scrolled_cb", "") # w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "") ##### END NEW def dbg(message, level=0, main_buffer=False, fout=False): """ send debug output to the slack-debug buffer and optionally write to a file. """ # TODO: do this smarter if level >= config.debug_level: global debug_string message = "DEBUG: {}".format(message) if fout: with open("/tmp/debug.log", "a+") as log_file: log_file.writelines(message + "\n") if main_buffer: w.prnt("", "slack: " + message) else: if slack_debug and (not debug_string or debug_string in message): w.prnt(slack_debug, message) ###### Config code class PluginConfig(object): Setting = namedtuple("Setting", ["default", "desc"]) # Default settings. # These are, initially, each a (default, desc) tuple; the former is the # default value of the setting, in the (string) format that weechat # expects, and the latter is the user-friendly description of the setting. # At __init__ time these values are extracted, the description is used to # set or update the setting description for use with /help, and the default # value is used to set the default for any settings not already defined. # Following this procedure, the keys remain the same, but the values are # the real (python) values of the settings. default_settings = { "auto_open_threads": Setting( default="false", desc="Automatically open threads when mentioned or in" " response to own messages.", ), "background_load_all_history": Setting( default="true", desc="Load the history for all channels in the background when the script is loaded," " rather than waiting until the buffer is switched to. You can set this to false if" " you experience performance issues, however that causes some loss of functionality," " see known issues in the readme.", ), "channel_name_typing_indicator": Setting( default="true", desc="Change the prefix of a channel from # to > when someone is" " typing in it. Note that this will (temporarily) affect the sort" " order if you sort buffers by name rather than by number.", ), "color_buflist_muted_channels": Setting( default="darkgray", desc="Color to use for muted channels in the buflist" ), "color_deleted": Setting( default="red", desc="Color to use for deleted messages and files." ), "color_edited_suffix": Setting( default="095", desc="Color to use for (edited) suffix on messages that have been edited.", ), "color_reaction_suffix": Setting( default="darkgray", desc="Color to use for the [:wave:(@user)] suffix on messages that" " have reactions attached to them.", ), "color_reaction_suffix_added_by_you": Setting( default="blue", desc="Color to use for reactions that you have added." ), "color_thread_suffix": Setting( default="lightcyan", desc="Color to use for the [thread: XXX] suffix on messages that" ' have threads attached to them. The special value "multiple" can' " be used to use a different color for each thread.", ), "color_typing_notice": Setting( default="yellow", desc="Color to use for the typing notice." ), "colorize_attachments": Setting( default="prefix", desc='Whether to colorize attachment lines. Values: "prefix": Only colorize' ' the prefix, "all": Colorize the whole line, "none": Don\'t colorize.', ), "colorize_private_chats": Setting( default="false", desc="Whether to use nick-colors in DM windows." ), "debug_level": Setting( default="3", desc="Show only this level of debug info (or higher) when" " debug_mode is on. Lower levels -> more messages.", ), "debug_mode": Setting( default="false", desc="Open a dedicated buffer for debug messages and start logging" " to it. How verbose the logging is depends on log_level.", ), "distracting_channels": Setting(default="", desc="List of channels to hide."), "external_user_suffix": Setting( default="*", desc="The suffix appended to nicks to indicate external users." ), "files_download_location": Setting( default="", desc="If set, file attachments will be automatically downloaded" ' to this location. "%h" will be replaced by WeeChat home,' ' "~/.weechat" by default. Requires WeeChat 2.2 or newer.', ), "group_name_prefix": Setting( default="&", desc="The prefix of buffer names for groups (private channels).", ), "history_fetch_count": Setting( default="200", desc="The number of messages to fetch for each channel when fetching" " history, between 1 and 1000.", ), "link_previews": Setting( default="true", desc="Show previews of website content linked by teammates." ), "log_channel_created": Setting( default="true", desc='Log "Channel created" in the Server buffer.', ), "map_underline_to": Setting( default="_", desc="When sending underlined text to slack, use this formatting" ' character for it. The default ("_") sends it as italics. Use' ' "*" to send bold instead.', ), "muted_channels_activity": Setting( default="personal_highlights", desc="Control which activity you see from muted channels, either" " none, personal_highlights, all_highlights or all. none: Don't" " show any activity. personal_highlights: Only show personal" " highlights, i.e. not @channel and @here. all_highlights: Show" " all highlights, but not other messages. all: Show all activity," " like other channels.", ), "never_away": Setting( default="false", desc='Poke Slack every five minutes so that it never marks you "away".', ), "notify_subscribed_threads": Setting( default="auto", desc="Control if you want to see a notification in the team buffer when a" " thread you're subscribed to receives a new message, either auto, true or" " false. auto means that you only get a notification if auto_open_threads" " and thread_messages_in_channel both are false. Defaults to auto.", ), "notify_usergroup_handle_updated": Setting( default="false", desc="Control if you want to see a notification in the team buffer when a" "usergroup's handle has changed, either true or false.", ), "record_events": Setting( default="false", desc="Log all traffic from Slack to disk as JSON." ), "render_bold_as": Setting( default="bold", desc="When receiving bold text from Slack, render it as this in WeeChat.", ), "render_emoji_as_string": Setting( default="false", desc="Render emojis as :emoji_name: instead of emoji characters. Enable this" " if your terminal doesn't support emojis, or set to 'both' if you want to" " see both renderings. Note that even though this is" " disabled by default, you need to place {}/blob/master/weemoji.json in your" " WeeChat directory to enable rendering emojis as emoji characters.".format( REPO_URL ), ), "render_italic_as": Setting( default="italic", desc="When receiving bold text from Slack, render it as this in WeeChat." ' If your terminal lacks italic support, consider using "underline" instead.', ), "send_typing_notice": Setting( default="true", desc="Alert Slack users when you are typing a message in the input bar " "(Requires reload)", ), "server_aliases": Setting( default="", desc="A comma separated list of `subdomain:alias` pairs. The alias" " will be used instead of the actual name of the slack (in buffer" " names, logging, etc). E.g `work:no_fun_allowed` would make your" " work slack show up as `no_fun_allowed` rather than `work.slack.com`.", ), "shared_name_prefix": Setting( default="%", desc="The prefix of buffer names for shared channels." ), "short_buffer_names": Setting( default="false", desc="Use `foo.#channel` rather than `foo.slack.com.#channel` as the" " internal name for Slack buffers.", ), "show_buflist_presence": Setting( default="true", desc="Display a `+` character in the buffer list for present users.", ), "show_reaction_nicks": Setting( default="false", desc="Display the name of the reacting user(s) alongside each reactji.", ), "slack_api_token": Setting( default="INSERT VALID KEY HERE!", desc="List of Slack API tokens, one per Slack instance you want to" " connect to; see the README for details on how to get these" " (note: content is evaluated, see /help eval).", ), "slack_timeout": Setting( default="20000", desc="How long (ms) to wait when communicating with Slack." ), "switch_buffer_on_join": Setting( default="true", desc="When /joining a channel, automatically switch to it as well.", ), "thread_broadcast_prefix": Setting( default="+ ", desc="Prefix to distinguish thread messages that were also sent " "to the channel, when thread_messages_in_channel is enabled.", ), "thread_messages_in_channel": Setting( default="false", desc="When enabled shows thread messages in the parent channel.", ), "unfurl_auto_link_display": Setting( default="both", desc='When displaying ("unfurling") links to channels/users/etc,' " determine what is displayed when the text matches the url" " without the protocol. This happens when Slack automatically" " creates links, e.g. from words separated by dots or email" ' addresses. Set it to "text" to only display the text written by' ' the user, "url" to only display the url or "both" (the default)' " to display both.", ), "unfurl_ignore_alt_text": Setting( default="false", desc='When displaying ("unfurling") links to channels/users/etc,' ' ignore the "alt text" present in the message and instead use the' " canonical name of the thing being linked to.", ), "unhide_buffers_with_activity": Setting( default="false", desc="When activity occurs on a buffer, unhide it even if it was" " previously hidden (whether by the user or by the" " distracting_channels setting).", ), "use_full_names": Setting( default="false", desc="Use full names as the nicks for all users. When this is" " false (the default), display names will be used if set, with a" " fallback to the full name if display name is not set.", ), "use_usernames": Setting( default="false", desc="Use usernames as the nicks for all users. Takes priority" " over use_full_names. Default false.", ), } # Set missing settings to their defaults. Load non-missing settings from # weechat configs. def __init__(self): self.settings = {} # Set all descriptions, replace the values in the dict with the # default setting value rather than the (setting,desc) tuple. for key, (default, desc) in self.default_settings.items(): w.config_set_desc_plugin(key, desc) self.settings[key] = default # Migrate settings from old versions of Weeslack... self.migrate() # ...and then set anything left over from the defaults. for key, default in self.settings.items(): if not w.config_get_plugin(key): w.config_set_plugin(key, default) self.config_changed(None, None, None) def __str__(self): return "".join( [x + "\t" + str(self.settings[x]) + "\n" for x in self.settings.keys()] ) def config_changed(self, data, full_key, value): if full_key is None: for key in self.settings: self.settings[key] = self.fetch_setting(key) else: key = full_key.replace(CONFIG_PREFIX + ".", "") self.settings[key] = self.fetch_setting(key) if ( full_key is None or full_key == CONFIG_PREFIX + ".debug_mode" ) and self.debug_mode: create_slack_debug_buffer() return w.WEECHAT_RC_OK def fetch_setting(self, key): try: return getattr(self, "get_" + key)(key) except AttributeError: # Most settings are on/off, so make get_boolean the default return self.get_boolean(key) except: # There was setting-specific getter, but it failed. print(format_exc_tb()) return self.settings[key] def __getattr__(self, key): try: return self.settings[key] except KeyError: raise AttributeError(key) def get_boolean(self, key): return w.config_string_to_boolean(w.config_get_plugin(key)) def get_string(self, key): return w.config_get_plugin(key) def get_int(self, key): return int(w.config_get_plugin(key)) def is_default(self, key): default = self.default_settings.get(key).default return w.config_get_plugin(key) == default get_color_buflist_muted_channels = get_string get_color_deleted = get_string get_color_edited_suffix = get_string get_color_reaction_suffix = get_string get_color_reaction_suffix_added_by_you = get_string get_color_thread_suffix = get_string get_color_typing_notice = get_string get_colorize_attachments = get_string get_debug_level = get_int get_external_user_suffix = get_string get_files_download_location = get_string get_group_name_prefix = get_string get_history_fetch_count = get_int get_map_underline_to = get_string get_muted_channels_activity = get_string get_thread_broadcast_prefix = get_string get_render_bold_as = get_string get_render_italic_as = get_string get_shared_name_prefix = get_string get_slack_timeout = get_int get_unfurl_auto_link_display = get_string def get_distracting_channels(self, key): return [x.strip() for x in w.config_get_plugin(key).split(",") if x] def get_server_aliases(self, key): alias_list = w.config_get_plugin(key) return dict(item.split(":") for item in alias_list.split(",") if ":" in item) def get_slack_api_token(self, key): token = w.config_get_plugin("slack_api_token") if token.startswith("${sec.data"): return w.string_eval_expression(token, {}, {}, {}) else: return token def get_string_or_boolean(self, key, *valid_strings): value = w.config_get_plugin(key) if value in valid_strings: return value return w.config_string_to_boolean(value) def get_notify_subscribed_threads(self, key): return self.get_string_or_boolean(key, "auto") def get_render_emoji_as_string(self, key): return self.get_string_or_boolean(key, "both") def migrate(self): """ This is to migrate the extension name from slack_extension to slack """ if not w.config_get_plugin("migrated"): for k in self.settings.keys(): if not w.config_is_set_plugin(k): p = w.config_get("{}_extension.{}".format(CONFIG_PREFIX, k)) data = w.config_string(p) if data != "": w.config_set_plugin(k, data) w.config_set_plugin("migrated", "true") old_thread_color_config = w.config_get_plugin("thread_suffix_color") new_thread_color_config = w.config_get_plugin("color_thread_suffix") if old_thread_color_config and not new_thread_color_config: w.config_set_plugin("color_thread_suffix", old_thread_color_config) def config_server_buffer_cb(data, key, value): for team in EVENTROUTER.teams.values(): team.buffer_merge(value) return w.WEECHAT_RC_OK # to Trace execution, add `setup_trace()` to startup # and to a function and sys.settrace(trace_calls) to a function def setup_trace(): global f now = time.time() f = open("{}/{}-trace.json".format(RECORD_DIR, now), "w") def trace_calls(frame, event, arg): global f if event != "call": return co = frame.f_code func_name = co.co_name if func_name == "write": # Ignore write() calls from print statements return func_line_no = frame.f_lineno func_filename = co.co_filename caller = frame.f_back caller_line_no = caller.f_lineno caller_filename = caller.f_code.co_filename print( "Call to %s on line %s of %s from line %s of %s" % (func_name, func_line_no, func_filename, caller_line_no, caller_filename), file=f, ) f.flush() return def get_rtm_connect_request(token, retries=3, team=None, callback=None): return SlackRequest( team, "rtm.connect", {"batch_presence_aware": 1}, retries=retries, token=token, callback=callback, ) def get_next_page(response_json): next_cursor = response_json.get("response_metadata", {}).get("next_cursor") if next_cursor: request = response_json["wee_slack_request_metadata"] request.post_data["cursor"] = next_cursor request.reset() EVENTROUTER.receive(request) return True else: return False def initiate_connection(token): initial_data = { "channels": [], "members": [], "usergroups": [], "remaining": { "channels": 2, "members": 1, "usergroups": 1, "prefs": 1, "presence": 1, }, "errors": [], } def handle_initial(data_type): def handle(response_json, eventrouter, team, channel, metadata): if not response_json["ok"]: if response_json["error"] == "user_is_restricted": w.prnt( "", "You are a restricted user in this team, " "{} not loaded".format(data_type), ) else: initial_data["errors"].append( "{}: {}".format(data_type, response_json["error"]) ) initial_data["remaining"][data_type] -= 1 create_team(token, initial_data) return initial_data[data_type].extend(response_json[data_type]) if not get_next_page(response_json): initial_data["remaining"][data_type] -= 1 create_team(token, initial_data) return handle def handle_prefs(response_json, eventrouter, team, channel, metadata): if not response_json["ok"]: initial_data["errors"].append("prefs: {}".format(response_json["error"])) initial_data["remaining"]["prefs"] -= 1 create_team(token, initial_data) return initial_data["prefs"] = response_json["prefs"] initial_data["remaining"]["prefs"] -= 1 create_team(token, initial_data) def handle_getPresence(response_json, eventrouter, team, channel, metadata): if not response_json["ok"]: initial_data["errors"].append("presence: {}".format(response_json["error"])) initial_data["remaining"]["presence"] -= 1 create_team(token, initial_data) return initial_data["presence"] = response_json initial_data["remaining"]["presence"] -= 1 create_team(token, initial_data) s = SlackRequest( None, "conversations.list", { "exclude_archived": True, "types": "public_channel,private_channel,im", "limit": 1000, }, token=token, callback=handle_initial("channels"), ) EVENTROUTER.receive(s) s = SlackRequest( None, "conversations.list", { "exclude_archived": True, "types": "mpim", "limit": 1000, }, token=token, callback=handle_initial("channels"), ) EVENTROUTER.receive(s) s = SlackRequest( None, "users.list", {"limit": 1000}, token=token, callback=handle_initial("members"), ) EVENTROUTER.receive(s) s = SlackRequest( None, "usergroups.list", {"include_users": True}, token=token, callback=handle_initial("usergroups"), ) EVENTROUTER.receive(s) s = SlackRequest( None, "users.prefs.get", token=token, callback=handle_prefs, ) EVENTROUTER.receive(s) s = SlackRequest( None, "users.getPresence", token=token, callback=handle_getPresence, ) EVENTROUTER.receive(s) def create_channel_from_info(eventrouter, channel_info, team, myidentifier, users): if channel_info.get("is_im"): return SlackDMChannel( eventrouter, users, myidentifier, team=team, **channel_info ) elif channel_info.get("is_mpim"): return SlackMPDMChannel( eventrouter, users, myidentifier, team=team, **channel_info ) elif channel_info.get("is_shared"): return SlackSharedChannel(eventrouter, team=team, **channel_info) elif channel_info.get("is_private"): return SlackPrivateChannel(eventrouter, team=team, **channel_info) else: return SlackChannel(eventrouter, team=team, **channel_info) def create_team(token, initial_data): if not any(initial_data["remaining"].values()): if initial_data["errors"]: w.prnt( "", "ERROR: Failed connecting to Slack with token {}: {}".format( token_for_print(token), ", ".join(initial_data["errors"]) ), ) if not re.match(r"^xo\w\w(-\d+){3}-[0-9a-f]+(:.*)?$", token): w.prnt( "", "ERROR: Token does not look like a valid Slack token. " "Ensure it is a valid token and not just a OAuth code.", ) return def handle_rtmconnect(response_json, eventrouter, team, channel, metadata): if not response_json["ok"]: print(response_json["error"]) return team_id = response_json["team"]["id"] myidentifier = response_json["self"]["id"] users = {} bots = {} for member in initial_data["members"]: if member.get("is_bot"): bots[member["id"]] = SlackBot(team_id, **member) else: users[member["id"]] = SlackUser(team_id, **member) self_nick = nick_from_profile( users[myidentifier].profile, response_json["self"]["name"] ) channels = {} for channel_info in initial_data["channels"]: channels[channel_info["id"]] = create_channel_from_info( eventrouter, channel_info, None, myidentifier, users ) subteams = {} for usergroup in initial_data["usergroups"]: is_member = myidentifier in usergroup["users"] subteams[usergroup["id"]] = SlackSubteam( team_id, is_member=is_member, **usergroup ) manual_presence = ( "away" if initial_data["presence"]["manual_away"] else "active" ) try: all_notifications_prefs = json.loads( initial_data["prefs"].get("all_notifications_prefs") ) global_keywords = all_notifications_prefs.get("global", {}).get( "global_keywords" ) except json.decoder.JSONDecodeError: global_keywords = None if global_keywords is None: print_error( "global_keywords not found in users.prefs.get", warning=True ) dbg( "global_keywords not found in users.prefs.get. Response of users.prefs.get: {}".format( json.dumps(initial_data["prefs"]) ), level=5, ) global_keywords = "" team_info = { "id": team_id, "name": response_json["team"]["id"], "domain": response_json["team"]["domain"], } team_hash = SlackTeam.generate_team_hash( team_id, response_json["team"]["domain"] ) if not eventrouter.teams.get(team_hash): team = SlackTeam( eventrouter, token, team_hash, response_json["url"], team_info, subteams, self_nick, myidentifier, manual_presence, users, bots, channels, muted_channels=initial_data["prefs"]["muted_channels"], highlight_words=global_keywords, ) eventrouter.register_team(team) team.connect() else: team = eventrouter.teams.get(team_hash) if team.myidentifier != myidentifier: print_error( "The Slack team {} has tokens for two different users, this is not supported. The " "token {} is for user {}, and the token {} is for user {}. Please remove one of " "them.".format( team.team_info["name"], token_for_print(team.token), team.nick, token_for_print(token), self_nick, ) ) else: print_error( "Ignoring duplicate Slack tokens for the same team ({}) and user ({}). The two " "tokens are {} and {}.".format( team.team_info["name"], team.nick, token_for_print(team.token), token_for_print(token), ), warning=True, ) s = get_rtm_connect_request(token, callback=handle_rtmconnect) EVENTROUTER.receive(s) if __name__ == "__main__": w = WeechatWrapper(weechat) if w.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "script_unloaded", "", ): weechat_version = int(w.info_get("version_number", "") or 0) weechat_upgrading = w.info_get("weechat_upgrading", "") completion_get_string = ( w.hook_completion_get_string if weechat_version < 0x2090000 else w.completion_get_string ) completion_list_add = ( w.hook_completion_list_add if weechat_version < 0x2090000 else w.completion_list_add ) if weechat_version < 0x2020000: w.prnt( "", "\nERROR: WeeChat version 2.2+ is required to use {}.\n\n".format( SCRIPT_NAME ), ) elif weechat_upgrading == "1": w.prnt( "", "NOTE: wee-slack will not work after running /upgrade until it's" " reloaded. Please run `/python reload slack` to continue using it. You" " will not receive any new messages in wee-slack buffers until doing this.", ) else: EVENTROUTER = EventRouter() receive_httprequest_callback = EVENTROUTER.receive_httprequest_callback receive_ws_callback = EVENTROUTER.receive_ws_callback # Global var section slack_debug = None config = PluginConfig() config_changed_cb = config.config_changed typing_timer = time.time() hide_distractions = False w.hook_config(CONFIG_PREFIX + ".*", "config_changed_cb", "") w.hook_config("irc.look.server_buffer", "config_server_buffer_cb", "") if weechat_version < 0x2090000: w.hook_modifier("input_text_for_buffer", "input_text_for_buffer_cb", "") EMOJI, EMOJI_WITH_SKIN_TONES_REVERSE = load_emoji() setup_hooks() if config.record_events: EVENTROUTER.record() hdata = Hdata(w) auto_connect = weechat.info_get("auto_connect", "") != "0" if auto_connect: tokens = [ token.strip() for token in config.slack_api_token.split(",") if token ] w.prnt( "", "Connecting to {} slack team{}.".format( len(tokens), "" if len(tokens) == 1 else "s" ), ) for t in tokens: if t.startswith("xoxc-") and ":" not in t: w.prnt( "", "{}When using an xoxc token, you need to also provide the d cookie in the format token:cookie".format( w.prefix("error") ), ) else: initiate_connection(t) EVENTROUTER.handle_next() weechat-scripts/python/weetris.py0000644000175100017510000004432215112622375016235 0ustar manumanu# # SPDX-FileCopyrightText: 2008-2025 Sébastien Helleu # SPDX-FileCopyrightText: 2009 drubin # SPDX-FileCopyrightText: 2010-2011 Trashlord # # SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Tetris game for WeeChat, yeah!""" SCRIPT_NAME = "weetris" SCRIPT_AUTHOR = "Sébastien Helleu " SCRIPT_VERSION = "1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Tetris game for WeeChat, yeah!" import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org/") import_ok = False try: import random import time except ImportError as exc: print(f"Missing package(s) for {SCRIPT_NAME}: {exc}") import_ok = False BUFFER_TITLE = ( f"{SCRIPT_NAME} {SCRIPT_VERSION} - enjoy! | " "Keys: arrows: " "move/rotate (ctrl-down: bottom), " "alt-n: new game, " "alt-p: pause | " "Input: q = quit" ) GAME_WIDTH = 10 GAME_HEIGHT = 20 MAX_LEVEL = 10 START_Y = 0 PIECES = ( 1024 + 512 + 64 + 32, # O 2048 + 1024 + 512 + 256, # I 2048 + 1024 + 512 + 64, # T 2048 + 1024 + 512 + 128, # L 2048 + 1024 + 512 + 32, # J 1024 + 512 + 128 + 64, # S 2048 + 1024 + 64 + 32, # Z ) PIECE_COLOR = { # < 256 colors False: [ "yellow", # O "lightcyan", # I "magenta", # T "brown", # L "blue", # J "green", # S "red", # Z ], # >= 256 colors True: [ "yellow", # O "lightcyan", # I "magenta", # T "172", # L "blue", # J "green", # S "red", # Z ], } PIECE_X_INC = (3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0) PIECE_Y_INC = (3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0) PIECE_ROTATION = ( 4096, 256, 16, 1, 8192, 512, 32, 2, 16384, 1024, 64, 4, 32768, 2048, 128, 8, ) # --------------------------------- --------------------------------- # | | | | | | | | | | # | 32768 | 16384 | 8192 | 4096 | | 8 | 128 | 2048 | 32768 | # | | | | | | | | | | # --------------------------------- --------------------------------- # | | | | | | | | | | # | 2048 | 1024 | 512 | 256 | | 4 | 64 | 1024 | 16384 | # | | | | | after | | | | | # --------------------------------- rotate --------------------------------- # | | | | | | | | | | # | 128 | 64 | 32 | 16 | =====> | 2 | 32 | 512 | 8192 | # | | | | | | | | | | # --------------------------------- --------------------------------- # | | | | | | | | | | # | 8 | 4 | 2 | 1 | | 1 | 16 | 256 | 4096 | # | | | | | | | | | | # --------------------------------- --------------------------------- weetris = { # will be set to True if 256 colors are supported by the terminal "256colors": False, "buffer": "", "timer": "", "level": 1, "matrix": [], "matrix_next": [], "playing": False, "paused": False, "lines": 0, "piece_x": 0, "piece_y": 0, "piece_number": -1, "next_piece_number": -1, "piece_form": 0, "best_level": 1, "best_lines": 0, "play_start_time": 0, "time_display_timer": "", } # script options weetris_settings_default = { "display_next_piece": ( "on", "display the next piece", ), "key_down_slow": ( "on", "the key arrow down moves the piece slowly: one position, and ctrl+down " "mores directly to the bottom; if disabled, the two keys are " "reversed", ), } weetris_settings = {} def weetris_config_cb(data, option, value): """Called when a script option is changed.""" pos = option.rfind(".") if pos > 0: name = option[pos + 1 :] if name in weetris_settings: weetris_settings[name] = value return weechat.WEECHAT_RC_OK def buffer_input_cb(data, buf, input_data): """Input on weetris buffer.""" if input_data == "q": weechat.buffer_close(weetris["buffer"]) return weechat.WEECHAT_RC_OK def buffer_close_cb(data, buf): """Weetris buffer closed (oh no, why?).""" weetris["buffer"] = "" if weetris["timer"]: weechat.unhook(weetris["timer"]) weetris["timer"] = "" if weetris["time_display_timer"]: weechat.unhook(weetris["time_display_timer"]) weetris["play_start_time"] = 0 weetris["time_display_timer"] = "" weechat.prnt("", "Thank you for playing WeeTris!") return weechat.WEECHAT_RC_OK def get_piece_block(value): """ Return a string with a single block of a piece (spaces with background color). """ if value < 0: block = weechat.color(",default") else: block = weechat.color("," + PIECE_COLOR[weetris["256colors"]][value]) return block + " " def display_line(y): """Display a line of the matrix.""" line = " │" if weetris["paused"]: if y == GAME_HEIGHT // 2: spaces_before = ((GAME_WIDTH * 2) - 6) // 2 spaces_after = (GAME_WIDTH * 2) - 6 - spaces_before line += (" " * spaces_before) + "PAUSED" + (" " * spaces_after) else: line += " " * GAME_WIDTH else: for x in range(GAME_WIDTH): line += get_piece_block(weetris["matrix"][y][x]) line += weechat.color(",default") + "│" if weetris["playing"] and weetris_settings["display_next_piece"] == "on": if y == 0: line += " Next: " elif 1 <= y <= 4: line += " " for x in range(4): line += get_piece_block(weetris["matrix_next"][y - 1][x]) line += weechat.color(",default") weechat.prnt_y(weetris["buffer"], START_Y + y + 1, line) def weetris_display_playing_time_cb(data, remaining_calls): """Callback of timer to display the playing time.""" total_seconds = time.time() - weetris["play_start_time"] minutes = int(total_seconds // 60) seconds = int(total_seconds % 60) total_seconds += 1 weechat.prnt_y( weetris["buffer"], START_Y + GAME_HEIGHT + 6, f" Playing time : {minutes:02d}:{seconds:02d}", ) return weechat.WEECHAT_RC_OK def display_level_lines(): """Display the current level and number of lines.""" str_lines = "lines" if weetris["lines"] > 1 else "line" list_info = [ f"Level {weetris['level']:-3d} {weetris['lines']:6d} {str_lines}", "-" * (1 + (GAME_WIDTH * 2) + 1), f"Highest level: {weetris['best_level']}", f"Max lines : {weetris['best_lines']}", ] for y, info in enumerate(list_info): weechat.prnt_y(weetris["buffer"], START_Y + GAME_HEIGHT + 2 + y, " " + info) def display_piece(display): """Display (or hide) the current piece.""" value = weetris["piece_number"] if display else -1 for i in range(16): if weetris["piece_form"] & (1 << i): x2 = weetris["piece_x"] + PIECE_X_INC[i] y2 = weetris["piece_y"] + PIECE_Y_INC[i] weetris["matrix"][y2][x2] = value def display_all(): """Display everything on the weetris buffer.""" display_piece(True) weechat.prnt_y(weetris["buffer"], START_Y, " ┌" + ("──" * GAME_WIDTH) + "┐") for y in range(GAME_HEIGHT): display_line(y) weechat.prnt_y( weetris["buffer"], START_Y + GAME_HEIGHT + 1, " └" + ("──" * GAME_WIDTH) + "┘" ) display_piece(False) def random_piece(): """Return a random piece number.""" return random.randint(0, len(PIECES) - 1) def set_matrix_next(): """Set the matrix for the next piece.""" weetris["matrix_next"] = [[-1] * 4 for i in range(4)] number = weetris["next_piece_number"] form = PIECES[number] for i in range(16): if form & (1 << i): weetris["matrix_next"][PIECE_Y_INC[i]][PIECE_X_INC[i]] = number def set_new_form(): """Choose a new random form.""" if weetris["next_piece_number"] < 0: weetris["next_piece_number"] = random_piece() weetris["piece_number"] = weetris["next_piece_number"] weetris["next_piece_number"] = random_piece() set_matrix_next() weetris["piece_form"] = PIECES[weetris["piece_number"]] weetris["piece_x"] = (GAME_WIDTH // 2) - 2 weetris["piece_y"] = 0 def init_timer(): """Initialize timer.""" if weetris["timer"]: weechat.unhook(weetris["timer"]) delay = max(100, 700 - ((weetris["level"] - 1) * 60)) weetris["timer"] = weechat.hook_timer(delay, 0, 0, "weetris_timer_cb", "") def new_game(): """New game.""" weechat.prnt_y(weetris["buffer"], START_Y + GAME_HEIGHT + 2, "") weetris["matrix"] = [[-1] * GAME_WIDTH for i in range(GAME_HEIGHT)] weetris["next_piece_number"] = -1 set_new_form() weetris["playing"] = True weetris["paused"] = False weetris["lines"] = 0 weetris["level"] = 1 weetris["play_start_time"] = time.time() weechat.prnt_y( weetris["buffer"], START_Y + GAME_HEIGHT + 6, " Playing time : 00:00" ) init_timer() weetris["time_display_timer"] = weechat.hook_timer( 1000, 0, 0, "weetris_display_playing_time_cb", "", ) display_all() display_level_lines() def rotation(form): """Rotate a form.""" new_form = 0 for i in range(16): if form & (1 << i): new_form |= PIECE_ROTATION[i] return new_form def is_possible(new_x, new_y, new_form): """Check if the "new_form" can be moved to position (new_x, new_y).""" for i in range(16): if not new_form & (1 << i): continue x = new_x + PIECE_X_INC[i] y = new_y + PIECE_Y_INC[i] if ( x < 0 or x >= GAME_WIDTH or y < 0 or y >= GAME_HEIGHT or weetris["matrix"][y][x] >= 0 ): return 0 return 1 def remove_completed_lines(): """Remove completed lines.""" y = GAME_HEIGHT - 1 lines_removed = False while y >= 0: if -1 not in weetris["matrix"][y]: for i in range(y, -1, -1): if i == 0: weetris["matrix"][i] = [-1] * GAME_WIDTH else: weetris["matrix"][i] = weetris["matrix"][i - 1] # Removes the line and increases the number of lines made # in the game in "lines" weetris["lines"] += 1 lines_removed = True if weetris["lines"] > weetris["best_lines"]: set_best("max_lines", weetris["lines"]) weetris["best_lines"] = weetris["lines"] else: y -= 1 if lines_removed: new_level = min(MAX_LEVEL, (weetris["lines"] // 10) + 1) if new_level != weetris["level"]: # Next level weetris["level"] = new_level if weetris["level"] > weetris["best_level"]: set_best("max_level", weetris["level"]) weetris["best_level"] = weetris["level"] init_timer() display_level_lines() def end_of_piece(): """End of a piece (it can not go down any more).""" display_piece(True) set_new_form() if is_possible(weetris["piece_x"], weetris["piece_y"], weetris["piece_form"]): remove_completed_lines() else: weetris["piece_form"] = 0 weetris["playing"] = False weetris["paused"] = False if weetris["time_display_timer"]: weechat.unhook(weetris["time_display_timer"]) weetris["time_display_timer"] = "" weechat.prnt_y( weetris["buffer"], START_Y + GAME_HEIGHT + 2, f">> End of game, score: {weetris['lines']} lines, " f"level weetris['level'] (alt-n to restart) <<" ) def weetris_init(): """Initialize weetris.""" keys = { "up": "up", "down": "down", "ctrl-down": "bottom", "left": "left", "right": "right", "meta-n": "new_game", "meta-p": "pause", } weetris["buffer"] = weechat.buffer_search("python", "weetris") if not weetris["buffer"]: weetris["buffer"] = weechat.buffer_new( "weetris", "buffer_input_cb", "", "buffer_close_cb", "" ) if weetris["buffer"]: weechat.buffer_set(weetris["buffer"], "type", "free") weechat.buffer_set(weetris["buffer"], "title", BUFFER_TITLE) for key, action in keys.items(): weechat.buffer_set( weetris["buffer"], f"key_bind_{key}", f"/weetris {action}" ) new_game() weechat.buffer_set(weetris["buffer"], "display", "1") def run_action(action): """Run an action, when a key is pressed on weetris buffer.""" if action == "rotate": new_form = rotation(weetris["piece_form"]) if is_possible(weetris["piece_x"], weetris["piece_y"], new_form): weetris["piece_form"] = new_form display_all() elif action == "left": if is_possible( weetris["piece_x"] - 1, weetris["piece_y"], weetris["piece_form"] ): weetris["piece_x"] -= 1 display_all() elif action == "right": if is_possible( weetris["piece_x"] + 1, weetris["piece_y"], weetris["piece_form"] ): weetris["piece_x"] += 1 display_all() elif action == "down": if is_possible( weetris["piece_x"], weetris["piece_y"] + 1, weetris["piece_form"] ): weetris["piece_y"] += 1 else: end_of_piece() display_all() elif action == "bottom": while is_possible( weetris["piece_x"], weetris["piece_y"] + 1, weetris["piece_form"] ): weetris["piece_y"] += 1 end_of_piece() display_all() def weetris_cmd_cb(data, buf, args): """Callback for command /weetris.""" if weetris["buffer"]: weechat.buffer_set(weetris["buffer"], "display", "1") else: weetris_init() if args == "new_game": new_game() elif args == "pause": if weetris["playing"]: weetris["paused"] = not weetris["paused"] display_all() else: if weetris["playing"] and not weetris["paused"]: key_slow_down = weetris_settings["key_down_slow"] == "on" actions = { "up": "rotate", "left": "left", "right": "right", "down": "down" if key_slow_down else "bottom", "bottom": "bottom" if key_slow_down else "down", } action = actions.get(args) if action: run_action(action) return weechat.WEECHAT_RC_OK def weetris_timer_cb(data, remaining_calls): """Weetris timer callback.""" if weetris["buffer"] and weetris["playing"] and not weetris["paused"]: if is_possible( weetris["piece_x"], weetris["piece_y"] + 1, weetris["piece_form"] ): weetris["piece_y"] += 1 else: end_of_piece() display_all() return weechat.WEECHAT_RC_OK def get_best(name, default=0): """Get the best level/lines.""" value = weechat.config_get_plugin(name) return int(value) if value else default def set_best(name, value): """Set the best level/lines.""" weechat.config_set_plugin(name, str(value)) def main(): """Main function.""" if weechat.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" ): term_colors = int(weechat.info_get("term_colors", "") or "8") weetris["256colors"] = term_colors >= 256 # set default settings version = weechat.info_get("version_number", "") or 0 for option, value in weetris_settings_default.items(): if weechat.config_is_set_plugin(option): weetris_settings[option] = weechat.config_get_plugin(option) else: weechat.config_set_plugin(option, value[0]) weetris_settings[option] = value[0] if int(version) >= 0x00030500: weechat.config_set_desc_plugin( option, f"{value[1]} (default: \"{value[0]}\")" ) # detect config changes weechat.hook_config( f"plugins.var.python.{SCRIPT_NAME}.*", "weetris_config_cb", "" ) # command /weetris weechat.hook_command( "weetris", "Run WeeTris", "", "Keys:\n" " arrow up: rotate current piece\n" " arrow left: move piece to the left\n" "arrow right: move piece to the right\n" " arrow down: increase speed of the piece\n" " ctrl+down: move piece to the bottom\n" " alt+n: restart the game\n" " alt+p: pause current game", "", "weetris_cmd_cb", "", ) # initialization if weechat.buffer_search("python", "weetris"): weetris_init() weetris["best_level"] = get_best("max_level", 1) weetris["best_lines"] = get_best("max_lines", 0) if __name__ == "__main__" and import_ok: main() weechat-scripts/python/hlpvitem.py0000644000175100017510000001677015112622376016412 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2009-2014 Sébastien Helleu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Item with highlight/private messages. # # After loading this script, you can add item "hlpv" to your status # bar with command: # /set weechat.bar.status.items [+tab] # then complete string by adding for example (without quotes): ",[hlpv]" # # History: # # 2014-05-10, Sébastien Helleu # version 0.3: change hook_print callback argument type of # displayed/highlight (WeeChat >= 1.0) # 2012-01-03, Sébastien Helleu : # version 0.2: make script compatible with Python 3.x # 2009-10-02, Sebastien Helleu : # version 0.1: initial release # SCRIPT_NAME = "hlpvitem" SCRIPT_AUTHOR = "Sebastien Helleu " SCRIPT_VERSION = "0.3" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Item with highlight/private messages" import_ok = True try: import weechat except: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False # script options hlpv_settings = { "show_all_buffers" : "off", # off = hidden buffers only "buffer_number" : "on", # display buffer number before name "buffer_short_name" : "on", # use buffer short name (if off, use full name) "highlight" : "on", # display highlights in item "private" : "on", # display privates in item "string_highlight" : "", # string displayed for highlight message (before buffer) "string_private" : "", # string displayed for private message (before buffer) "string_delimiter" : " > ", # delimiter between prefix and message "color_string_highlight": "", # color for string_highlight (by default == weechat.color.status_data_highlight) "color_string_private" : "", # color for string_private (by default == weechat.color.status_data_private) "color_buffer_number" : "", # color for buffer number (by default == weechat.color.status_highlight/private) "color_buffer_name" : "default", # color for buffer name "color_prefix" : "white", # color for prefix "color_delimiter" : "cyan", # color for delimiter "color_message" : "default", # color for message "visible_seconds" : "7", # amount of seconds each message is visible } hlpv_messages = [] def hlpv_timer(): weechat.hook_timer(int(weechat.config_get_plugin("visible_seconds")) * 1000, 0, 1, "hlpv_timer_cb", "") def hlpv_timer_cb(data, remaining_calls): """ Called when a message must be removed from list. """ global hlpv_messages if len(hlpv_messages): hlpv_messages.pop(0) weechat.bar_item_update("hlpv") if len(hlpv_messages) > 0: hlpv_timer() return weechat.WEECHAT_RC_OK def hlpv_item_add(buffer, highlight, prefix, message): """ Add message to list of messages (will be displayed by item). """ global hlpv_messages if highlight: color_type = weechat.config_string(weechat.config_get("weechat.color.status_data_highlight")) color_string_highlight = weechat.config_get_plugin("color_string_highlight") if color_string_highlight == "": color_string_highlight = color_type string_prefix = "%s%s" % (weechat.color(color_string_highlight), weechat.config_get_plugin("string_highlight")) else: color_type = weechat.config_string(weechat.config_get("weechat.color.status_data_private")) color_string_private = weechat.config_get_plugin("color_string_private") if color_string_private == "": color_string_private = color_type string_prefix = "%s%s" % (weechat.color(color_string_private), weechat.config_get_plugin("string_private")) color_delimiter = weechat.color(weechat.config_get_plugin("color_delimiter")) if weechat.config_get_plugin("buffer_number") == "on": color_buffer_number = weechat.config_get_plugin("color_buffer_number") if color_buffer_number == "": color_buffer_number = color_type buffer_number = "%s%s%s:" % (weechat.color(color_buffer_number), weechat.buffer_get_integer(buffer, "number"), color_delimiter) else: buffer_number = "" color_buffer_name = weechat.color(weechat.config_get_plugin("color_buffer_name")) if weechat.config_get_plugin("buffer_short_name") == "on": buffer_name = weechat.buffer_get_string(buffer, "short_name") else: buffer_name = weechat.buffer_get_string(buffer, "name") color_prefix = weechat.color(weechat.config_get_plugin("color_prefix")) string_delimiter = weechat.config_get_plugin("string_delimiter") color_message = weechat.color(weechat.config_get_plugin("color_message")) string = "%s%s%s%s: %s%s%s%s%s%s" % (string_prefix, buffer_number, color_buffer_name, buffer_name, color_prefix, prefix, color_delimiter, string_delimiter, color_message, message) if len(hlpv_messages) == 0: hlpv_timer() hlpv_messages.append(string) weechat.bar_item_update("hlpv") def hlpv_print_cb(data, buffer, date, tags, displayed, highlight, prefix, message): """ Called when a message is printed. """ tagslist = tags.split(",") show_all_buffers = weechat.config_get_plugin("show_all_buffers") num_displayed = weechat.buffer_get_integer(buffer, "num_displayed") if num_displayed == 0 or show_all_buffers == "on": highlight_enabled = weechat.config_get_plugin("highlight") private_enabled = weechat.config_get_plugin("private") if (int(highlight) and (highlight_enabled == "on")) or (("notify_private" in tagslist) and (private_enabled == "on")): hlpv_item_add(buffer, int(highlight), prefix, message) return weechat.WEECHAT_RC_OK def hlpv_item_cb(data, buffer, args): """ Callback for building hlpv item. """ global hlpv_messages if len(hlpv_messages) > 0: return hlpv_messages[0] return "" if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): # set default settings for option, default_value in hlpv_settings.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, default_value) # new item weechat.bar_item_new('hlpv', 'hlpv_item_cb', '') # hook all printed messages weechat.hook_print("", "", "", 1, "hlpv_print_cb", "") weechat-scripts/python/fish.py0000644000175100017510000010626315112622402015476 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2011-2022 David Flatz # Copyright (C) 2017-2020 Marcin Kurczewski # Copyright (C) 2017 Ricardo Ferreira # Copyright (C) 2014 Charles Franklin # Copyright (C) 2012 Markus Näsman # Copyright (C) 2009 Bjorn Edstrom # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # NOTE: Blowfish and DH1080 implementation is licenced under a different # license: # # Copyright (c) 2009, Bjorn Edstrom # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # # Suggestions, Bugs, ...? # https://github.com/freshprince/weechat-fish # # NOTE ABOUT DH1080: # ================= # # Diffie-Hellman key exchange assumes that you already have # authenticated channels between Alice and Bob. Which means that Alice # has to be sure that she is really talking to Bob and not to any man in # the middle. But since the whole idea of FiSH is that you want to # encrypt your communication on the IRC server whose operators you do # not trust, there is no reliable way for Alice to tell if she really is # talking to Bob. It could also be some rogue IRC admin impersonating # Bob with a fake hostname and ident or even doing a MITM attack on # DH1080. This means you can consider using DH1080 key exchange over # IRC utterly broken in terms of security. # import re import struct import hashlib import base64 import sys from os import urandom SCRIPT_NAME = "fish" SCRIPT_AUTHOR = "David Flatz " SCRIPT_VERSION = "0.15" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "FiSH for weechat" CONFIG_FILE_NAME = SCRIPT_NAME import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org/") import_ok = False try: import Crypto.Cipher.Blowfish as CryptoBlowfish except ImportError: try: import Cryptodome.Cipher.Blowfish as CryptoBlowfish except ImportError: print("Pycryptodome must be installed to use fish") import_ok = False # # GLOBALS # fish_config_file = None fish_config_section = {} fish_config_option = {} fish_keys = {} fish_cyphers = {} fish_DH1080ctx = {} fish_encryption_announced = {} fish_secure_key = "" fish_secure_cipher = None # # CONFIG # def fish_config_reload_cb(data, config_file): return weechat.config_reload(config_file) def fish_config_keys_read_cb(data, config_file, section_name, option_name, value): global fish_keys option = weechat.config_new_option( config_file, section_name, option_name, "string", "key", "", 0, 0, "", value, 0, "", "", "", "", "", "") if not option: return weechat.WEECHAT_CONFIG_OPTION_SET_ERROR fish_keys[option_name] = value return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED def fish_config_keys_write_cb(data, config_file, section_name): global fish_keys, fish_secure_cipher weechat.config_write_line(config_file, section_name, "") for target, key in sorted(fish_keys.items()): if fish_secure_cipher is not None: weechat.config_write_line( config_file, blowcrypt_pack(target.encode(), fish_secure_cipher), blowcrypt_pack(key.encode(), fish_secure_cipher)) else: weechat.config_write_line(config_file, target, key) return weechat.WEECHAT_RC_OK def fish_config_init(): global fish_config_file, fish_config_section, fish_config_option global fish_secure_cipher fish_config_file = weechat.config_new( CONFIG_FILE_NAME, "fish_config_reload_cb", "") if not fish_config_file: return # look fish_config_section["look"] = weechat.config_new_section( fish_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", "") if not fish_config_section["look"]: weechat.config_free(fish_config_file) return fish_config_option["announce"] = weechat.config_new_option( fish_config_file, fish_config_section["look"], "announce", "boolean", "announce if messages are being encrypted or not", "", 0, 0, "on", "on", 0, "", "", "", "", "", "") fish_config_option["marker"] = weechat.config_new_option( fish_config_file, fish_config_section["look"], "marker", "string", "marker for important FiSH messages", "", 0, 0, "O<", "O<", 0, "", "", "", "", "", "") fish_config_option["mark_position"] = weechat.config_new_option( fish_config_file, fish_config_section["look"], "mark_position", "integer", "put marker for encrypted INCOMING messages at start or end", "off|begin|end", 0, 2, "off", "off", 0, "", "", "", "", "", "") fish_config_option["mark_encrypted"] = weechat.config_new_option( fish_config_file, fish_config_section["look"], "mark_encrypted", "string", "marker for encrypted INCOMING messages", "", 0, 0, "*", "*", 0, "", "", "", "", "", "") # color fish_config_section["color"] = weechat.config_new_section( fish_config_file, "color", 0, 0, "", "", "", "", "", "", "", "", "", "") if not fish_config_section["color"]: weechat.config_free(fish_config_file) return fish_config_option["alert"] = weechat.config_new_option( fish_config_file, fish_config_section["color"], "alert", "color", "color for important FiSH message markers", "", 0, 0, "lightblue", "lightblue", 0, "", "", "", "", "", "") # secure fish_config_section["secure"] = weechat.config_new_section( fish_config_file, "secure", 0, 0, "", "", "", "", "", "", "", "", "", "") if not fish_config_section["secure"]: weechat.config_free(fish_config_file) return fish_config_option["key"] = weechat.config_new_option( fish_config_file, fish_config_section["secure"], "key", "string", "key for securing blowfish keys", "", 0, 0, "", "", 0, "", "", "", "", "", "") # keys fish_config_section["keys"] = weechat.config_new_section( fish_config_file, "keys", 0, 0, "fish_config_keys_read_cb", "", "fish_config_keys_write_cb", "", "", "", "", "", "", "") if not fish_config_section["keys"]: weechat.config_free(fish_config_file) return def fish_config_read(): global fish_config_file return weechat.config_read(fish_config_file) def fish_config_write(): global fish_config_file return weechat.config_write(fish_config_file) ## # Blowfish and DH1080 Code: ## # # BLOWFISH # class Blowfish: def __init__(self, key=None): if key: if len(key) > 72: key = key[:72] self.blowfish = CryptoBlowfish.new( key.encode('utf-8'), CryptoBlowfish.MODE_ECB) def decrypt(self, data): return self.blowfish.decrypt(data) def encrypt(self, data): return self.blowfish.encrypt(data) # XXX: Unstable. def blowcrypt_b64encode(s): """A non-standard base64-encode.""" B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" res = '' while s: left, right = struct.unpack('>LL', s[:8]) for i in range(6): res += B64[right & 0x3f] right >>= 6 for i in range(6): res += B64[left & 0x3f] left >>= 6 s = s[8:] return res def blowcrypt_b64decode(s): """A non-standard base64-decode.""" B64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" res = [] while s: left, right = 0, 0 for i, p in enumerate(s[0:6]): right |= B64.index(p) << (i * 6) for i, p in enumerate(s[6:12]): left |= B64.index(p) << (i * 6) for i in range(0, 4): res.append((left & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8)) for i in range(0, 4): res.append((right & (0xFF << ((3 - i) * 8))) >> ((3 - i) * 8)) s = s[12:] return bytes(res) def padto(msg, length): """Pads 'msg' with zeroes until it's length is divisible by 'length'. If the length of msg is already a multiple of 'length', does nothing.""" L = len(msg) if L % length: msg += b'\x00' * (length - L % length) assert len(msg) % length == 0 return msg def blowcrypt_pack(msg, cipher): """.""" return '+OK ' + blowcrypt_b64encode(cipher.encrypt(padto(msg, 8))) def blowcrypt_unpack(msg, cipher, key): """.""" if not (msg.startswith('+OK ') or msg.startswith('mcps ')): raise ValueError _, rest = msg.split(' ', 1) if rest.startswith('*'): # CBC mode rest = rest[1:] if len(rest) % 4: rest += '=' * (4 - len(rest) % 4) raw = base64.b64decode(rest) iv = raw[:8] raw = raw[8:] cbcCipher = CryptoBlowfish.new( key.encode('utf-8'), CryptoBlowfish.MODE_CBC, iv) plain = cbcCipher.decrypt(padto(raw, 8)) else: if len(rest) < 12: raise ValueError if not (len(rest) % 12) == 0: rest = rest[:-(len(rest) % 12)] try: raw = blowcrypt_b64decode(padto(rest, 12)) except TypeError: raise ValueError if not raw: raise ValueError plain = cipher.decrypt(raw) return plain.strip(b'\x00').replace(b'\n', b'') # # DH1080 # g_dh1080 = 2 p_dh1080 = int('FBE1022E23D213E8ACFA9AE8B9DFAD' 'A3EA6B7AC7A7B7E95AB5EB2DF85892' '1FEADE95E6AC7BE7DE6ADBAB8A783E' '7AF7A7FA6A2B7BEB1E72EAE2B72F9F' 'A2BFB2A2EFBEFAC868BADB3E828FA8' 'BADFADA3E4CC1BE7E8AFE85E9698A7' '83EB68FA07A77AB6AD7BEB618ACF9C' 'A2897EB28A6189EFA07AB99A8A7FA9' 'AE299EFA7BA66DEAFEFBEFBF0B7D8B', 16) q_dh1080 = (p_dh1080 - 1) // 2 def dh1080_b64encode(s): """A non-standard base64-encode.""" b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" d = [0] * len(s) * 2 L = len(s) * 8 m = 0x80 i, j, k, t = 0, 0, 0, 0 while i < L: if s[i >> 3] & m: t |= 1 j += 1 m >>= 1 if not m: m = 0x80 if not j % 6: d[k] = b64[t] t &= 0 k += 1 t <<= 1 t %= 0x100 # i += 1 m = 5 - j % 6 t <<= m t %= 0x100 if m: d[k] = b64[t] k += 1 d[k] = 0 res = '' for q in d: if q == 0: break res += q return res def dh1080_b64decode(s): """A non-standard base64-encode.""" b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" buf = [0] * 256 for i in range(64): buf[ord(b64[i])] = i L = len(s) if L < 2: raise ValueError for i in reversed(list(range(L - 1))): if buf[ord(s[i])] == 0: L -= 1 else: break if L < 2: raise ValueError d = [0] * L i, k = 0, 0 while True: i += 1 if k + 1 < L: d[i - 1] = buf[ord(s[k])] << 2 d[i - 1] %= 0x100 else: break k += 1 if k < L: d[i - 1] |= buf[ord(s[k])] >> 4 else: break i += 1 if k + 1 < L: d[i - 1] = buf[ord(s[k])] << 4 d[i - 1] %= 0x100 else: break k += 1 if k < L: d[i - 1] |= buf[ord(s[k])] >> 2 else: break i += 1 if k + 1 < L: d[i - 1] = buf[ord(s[k])] << 6 d[i - 1] %= 0x100 else: break k += 1 if k < L: d[i - 1] |= buf[ord(s[k])] % 0x100 else: break k += 1 return bytes(d[0:i - 1]) def dh_validate_public(public, q, p): """See RFC 2631 section 2.1.5.""" return 1 == pow(public, q, p) class DH1080Ctx: """DH1080 context.""" def __init__(self): self.public = 0 self.private = 0 self.secret = 0 self.state = 0 bits = 1080 while True: self.private = bytes2int(urandom(bits // 8)) self.public = pow(g_dh1080, self.private, p_dh1080) if 2 <= self.public <= p_dh1080 - 1 and \ dh_validate_public(self.public, q_dh1080, p_dh1080) == 1: break def dh1080_pack(ctx): """.""" cmd = None if ctx.state == 0: ctx.state = 1 cmd = "DH1080_INIT " else: cmd = "DH1080_FINISH " return cmd + dh1080_b64encode(int2bytes(ctx.public)) def dh1080_unpack(msg, ctx): """.""" if not msg.startswith("DH1080_"): raise ValueError if ctx.state == 0: if not msg.startswith("DH1080_INIT "): raise ValueError ctx.state = 1 try: cmd, public_raw = msg.split(' ', 1) public = bytes2int(dh1080_b64decode(public_raw)) if not 1 < public < p_dh1080: raise ValueError ctx.secret = pow(public, ctx.private, p_dh1080) except Exception: raise ValueError elif ctx.state == 1: if not msg.startswith("DH1080_FINISH "): raise ValueError ctx.state = 1 try: cmd, public_raw = msg.split(' ', 1) public = bytes2int(dh1080_b64decode(public_raw)) if not 1 < public < p_dh1080: raise ValueError ctx.secret = pow(public, ctx.private, p_dh1080) except Exception: raise ValueError return True def dh1080_secret(ctx): """.""" if ctx.secret == 0: raise ValueError return dh1080_b64encode(sha256(int2bytes(ctx.secret))) def bytes2int(b): """Variable length big endian to integer.""" return int.from_bytes(b, byteorder='big') def int2bytes(n): """Integer to variable length big endian.""" return n.to_bytes((n.bit_length() + 7) // 8, byteorder='big') def sha256(s): """sha256""" return hashlib.sha256(s).digest() ## # END Blowfish and DH1080 Code ## # # HOOKS # def fish_secure_key_cb(data, option, value): global fish_secure_key, fish_secure_cipher fish_secure_key = weechat.config_string( weechat.config_get("fish.secure.key")) if fish_secure_key == "": fish_secure_cipher = None return weechat.WEECHAT_RC_OK if fish_secure_key[:6] == "${sec.": decrypted = weechat.string_eval_expression(fish_secure_key, {}, {}, {}) if decrypted: fish_secure_cipher = Blowfish(decrypted) return weechat.WEECHAT_RC_OK else: weechat.config_option_set(fish_config_option["key"], "", 0) weechat.prnt("", "Decrypt sec.conf first\n") return weechat.WEECHAT_RC_OK if fish_secure_key != "": fish_secure_cipher = Blowfish(fish_secure_key) return weechat.WEECHAT_RC_OK def fish_modifier_in_notice_cb(data, modifier, server_name, string): global fish_DH1080ctx, fish_keys, fish_cyphers if type(string) is bytes: return string match = re.match( r"^((?:@[^ ]* )?:(.*?)!.*? NOTICE (.*?) :)" r"((DH1080_INIT |DH1080_FINISH |\+OK |mcps )?.*)$", string) # match.group(0): message # match.group(1): msg without payload # match.group(2): source # match.group(3): target # match.group(4): msg # match.group(5): "DH1080_INIT "|"DH1080_FINISH "|"+OK "|"mcps " if not match or not match.group(5): return string if match.group(3) != weechat.info_get("irc_nick", server_name): return string target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() buffer = weechat.info_get("irc_buffer", "%s,%s" % ( server_name, match.group(2))) if match.group(5) == "DH1080_FINISH " and targetl in fish_DH1080ctx: if not dh1080_unpack(match.group(4), fish_DH1080ctx[targetl]): fish_announce_unencrypted(buffer, target) return string fish_alert(buffer, "Key exchange for %s successful" % target) fish_keys[targetl] = dh1080_secret(fish_DH1080ctx[targetl]) if targetl in fish_cyphers: del fish_cyphers[targetl] del fish_DH1080ctx[targetl] return "" if match.group(5) == "DH1080_INIT ": fish_DH1080ctx[targetl] = DH1080Ctx() msg = ' '.join(match.group(4).split()[0:2]) if not dh1080_unpack(msg, fish_DH1080ctx[targetl]): fish_announce_unencrypted(buffer, target) return string reply = dh1080_pack(fish_DH1080ctx[targetl]) fish_alert(buffer, "Key exchange initiated by %s. Key set." % target) weechat.command(buffer, "/mute -all notice %s %s" % ( match.group(2), reply)) fish_keys[targetl] = dh1080_secret(fish_DH1080ctx[targetl]) if targetl in fish_cyphers: del fish_cyphers[targetl] del fish_DH1080ctx[targetl] return "" if match.group(5) in ["+OK ", "mcps "]: if targetl not in fish_keys: fish_announce_unencrypted(buffer, target) return string key = fish_keys[targetl] try: if targetl not in fish_cyphers: b = Blowfish(key) fish_cyphers[targetl] = b else: b = fish_cyphers[targetl] clean = blowcrypt_unpack(match.group(4), b, key) fish_announce_encrypted(buffer, target) return b"%s%s" % ( match.group(1).encode(), fish_msg_w_marker(clean)) except Exception as e: fish_announce_unencrypted(buffer, target) raise e fish_announce_unencrypted(buffer, target) return string def fish_modifier_in_privmsg_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers if type(string) is bytes: return string match = re.match( r"^((?:@[^ ]* )?:(.*?)!.*? PRIVMSG (.*?) :)(\x01ACTION )?" r"((\+OK |mcps )?.*?)(\x01)?$", string) # match.group(0): message # match.group(1): msg without payload # match.group(2): source # match.group(3): target # match.group(4): action # match.group(5): msg # match.group(6): "+OK "|"mcps " if not match: return string if match.group(3) == weechat.info_get("irc_nick", server_name): dest = match.group(2) else: dest = match.group(3) target = "%s/%s" % (server_name, dest) targetl = ("%s/%s" % (server_name, dest)).lower() buffer = weechat.info_get("irc_buffer", "%s,%s" % (server_name, dest)) if not match.group(6): fish_announce_unencrypted(buffer, target) return string if targetl not in fish_keys: fish_announce_unencrypted(buffer, target) return string key = fish_keys[targetl] try: if targetl not in fish_cyphers: b = Blowfish(key) fish_cyphers[targetl] = b else: b = fish_cyphers[targetl] clean = blowcrypt_unpack(match.group(5), b, key) fish_announce_encrypted(buffer, target) if not match.group(4): return b'%s%s' % ( match.group(1).encode(), fish_msg_w_marker(clean)) return b"%s%s%s\x01" % ( match.group(1).encode(), match.group(4).encode(), fish_msg_w_marker(clean)) except Exception as e: fish_announce_unencrypted(buffer, target) raise e def fish_modifier_in_topic_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers if type(string) is bytes: return string match = re.match(r"^((?:@[^ ]* )?:.*?!.*? TOPIC (.*?) :)((\+OK |mcps )?.*)$", string) # match.group(0): message # match.group(1): msg without payload # match.group(2): channel # match.group(3): topic # match.group(4): "+OK "|"mcps " if not match: return string target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() buffer = weechat.info_get("irc_buffer", "%s,%s" % ( server_name, match.group(2))) if targetl not in fish_keys or not match.group(4): fish_announce_unencrypted(buffer, target) return string key = fish_keys[targetl] try: if targetl not in fish_cyphers: b = Blowfish(key) fish_cyphers[targetl] = b else: b = fish_cyphers[targetl] clean = blowcrypt_unpack(match.group(3), b, key) fish_announce_encrypted(buffer, target) return b"%s%s" % (match.group(1).encode(), fish_msg_w_marker(clean)) except Exception as e: fish_announce_unencrypted(buffer, target) raise e def fish_modifier_in_332_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers if type(string) is bytes: return string match = re.match(r"^((?:@[^ ]* )?:.*? 332 .*? (.*?) :)((\+OK |mcps )?.*)$", string) if not match: return string target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() buffer = weechat.info_get("irc_buffer", "%s,%s" % ( server_name, match.group(2))) if targetl not in fish_keys or not match.group(4): fish_announce_unencrypted(buffer, target) return string key = fish_keys[targetl] try: if targetl not in fish_cyphers: b = Blowfish(key) fish_cyphers[targetl] = b else: b = fish_cyphers[targetl] clean = blowcrypt_unpack(match.group(3), b, key) fish_announce_encrypted(buffer, target) return b"%s%s" % (match.group(1).encode(), fish_msg_w_marker(clean)) except Exception as e: fish_announce_unencrypted(buffer, target) raise e def fish_modifier_out_privmsg_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers if type(string) is bytes: return string match = re.match(r"^(PRIVMSG (.*?) :)(.*)$", string) if not match: return string target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() buffer = weechat.info_get("irc_buffer", "%s,%s" % ( server_name, match.group(2))) if targetl not in fish_keys: fish_announce_unencrypted(buffer, target) return string if targetl not in fish_cyphers: b = Blowfish(fish_keys[targetl]) fish_cyphers[targetl] = b else: b = fish_cyphers[targetl] cypher = blowcrypt_pack(fish_msg_wo_marker(match.group(3)).encode(), b) fish_announce_encrypted(buffer, target) return "%s%s" % (match.group(1), cypher) def fish_modifier_out_topic_cb(data, modifier, server_name, string): global fish_keys, fish_cyphers if type(string) is bytes: return string match = re.match(r"^(TOPIC (.*?) :)(.*)$", string) if not match: return string if not match.group(3): return string target = "%s/%s" % (server_name, match.group(2)) targetl = ("%s/%s" % (server_name, match.group(2))).lower() buffer = weechat.info_get("irc_buffer", "%s,%s" % ( server_name, match.group(2))) if targetl not in fish_keys: fish_announce_unencrypted(buffer, target) return string if targetl not in fish_cyphers: b = Blowfish(fish_keys[targetl]) fish_cyphers[targetl] = b else: b = fish_cyphers[targetl] cypher = blowcrypt_pack(match.group(3).encode(), b) fish_announce_encrypted(buffer, target) return "%s%s" % (match.group(1), cypher) def fish_modifier_input_text(data, modifier, server_name, string): if weechat.string_is_command_char(string): return string buffer = weechat.current_buffer() name = weechat.buffer_get_string(buffer, "name") target = name.replace(".", "/") targetl = target.lower() if targetl not in fish_keys: return string return "%s" % (fish_msg_w_marker(string.encode()).decode()) def fish_unload_cb(): fish_config_write() return weechat.WEECHAT_RC_OK # # COMMANDS # def fish_cmd_blowkey(data, buffer, args): global fish_keys, fish_cyphers, fish_DH1080ctx, fish_config_option global fish_secure_cipher if args == "" or args == "list": fish_list_keys(buffer) return weechat.WEECHAT_RC_OK elif args == "genkey": fish_secure_genkey(buffer) return weechat.WEECHAT_RC_OK argv = args.split(" ") if (len(argv) > 2 and argv[1] == "-server"): server_name = argv[2] del argv[2] del argv[1] pos = args.find(" ") pos = args.find(" ", pos + 1) args = args[pos+1:] else: server_name = weechat.buffer_get_string(buffer, "localvar_server") buffer_type = weechat.buffer_get_string(buffer, "localvar_type") # if no target user has been specified grab the one from the buffer if it # is private if argv[0] == "exchange" and len(argv) == 1 and buffer_type == "private": target_user = weechat.buffer_get_string(buffer, "localvar_channel") elif (argv[0] == "set" and (buffer_type == "private" or buffer_type == "channel") and len(argv) == 2): target_user = weechat.buffer_get_string(buffer, "localvar_channel") elif len(argv) < 2: return weechat.WEECHAT_RC_ERROR else: target_user = argv[1] argv2eol = "" pos = args.find(" ") if pos: pos = args.find(" ", pos + 1) if pos > 0: argv2eol = args[pos + 1:] else: argv2eol = args[args.find(" ") + 1:] target = "%s/%s" % (server_name, target_user) targetl = ("%s/%s" % (server_name, target_user)).lower() if argv[0] == "set": fish_keys[targetl] = argv2eol if targetl in fish_cyphers: del fish_cyphers[targetl] weechat.prnt(buffer, "set key for %s to %s" % (target, argv2eol)) return weechat.WEECHAT_RC_OK if argv[0] == "remove": if not len(argv) == 2: return weechat.WEECHAT_RC_ERROR if targetl not in fish_keys: return weechat.WEECHAT_RC_ERROR del fish_keys[targetl] if targetl in fish_cyphers: del fish_cyphers[targetl] weechat.prnt(buffer, "removed key for %s" % target) return weechat.WEECHAT_RC_OK if argv[0] == "exchange": if server_name == "": return weechat.WEECHAT_RC_ERROR weechat.prnt(buffer, "Initiating DH1080 Exchange with %s" % target) fish_DH1080ctx[targetl] = DH1080Ctx() msg = dh1080_pack(fish_DH1080ctx[targetl]) weechat.command(buffer, "/mute -all notice -server %s %s %s" % ( server_name, target_user, msg)) return weechat.WEECHAT_RC_OK return weechat.WEECHAT_RC_ERROR # # HELPERS # def fish_secure(): global fish_secure_key, fish_secure_cipher fish_secure_key = weechat.config_string(fish_config_option["key"]) # if blank, do nothing if fish_secure_key == "": return # if ${sec.data.fish}, check if sec.conf is decrypted # and decrypt elif fish_secure_key[:6] == "${sec.": decrypted = weechat.string_eval_expression(fish_secure_key, {}, {}, {}) if decrypted: fish_secure_cipher = Blowfish(decrypted) fish_decrypt_keys() return else: global SCRIPT_NAME message = ("\n%s%sblowkey:%s unable to recover key from sec.conf\n" "%s%sblowkey:%s fish.py %sNOT LOADED\n" "%s%sblowkey:%s decrypt secured data first\n" "%s%sblowkey:%s then reload fish.py\n\n") % ( weechat.prefix("error"), weechat.color("underline"), weechat.color("reset"), weechat.prefix("error"), weechat.color("underline"), weechat.color("reset"), weechat.color("*red"), weechat.prefix("error"), weechat.color("underline"), weechat.color("reset"), weechat.prefix("error"), weechat.color("underline"), weechat.color("reset")) weechat.prnt("", "%s" % message) weechat.command(weechat.current_buffer(), "/wait 1ms /python unload %s" % SCRIPT_NAME) return # if key is neither ${sec.data.fish} or "" # encrypt/decrypt with user supplied, plain text key if fish_secure_key != "": fish_secure_cipher = Blowfish(fish_secure_key) fish_decrypt_keys() return def fish_decrypt_keys(): global fish_keys, fish_secure_cipher global fish_cyphers fish_keys_tmp = {} for target, key in fish_keys.iteritems(): fish_keys_tmp[blowcrypt_unpack( target, fish_secure_cipher)] = blowcrypt_unpack( key, fish_secure_cipher) fish_keys = fish_keys_tmp def fish_secure_genkey(buffer): global fish_secure_cipher, fish_config_option newKey = blowcrypt_b64encode(urandom(32)) # test to see if sec.conf decrypted weechat.command(buffer, "/secure set fish test") decrypted = weechat.string_eval_expression("${sec.data.fish}", {}, {}, {}) if decrypted == "test": weechat.config_option_set(fish_config_option["key"], "${sec.data.fish}", 0) fish_secure_cipher = Blowfish(newKey) weechat.command(buffer, "/secure set fish %s" % newKey) def fish_announce_encrypted(buffer, target): global fish_encryption_announced, fish_config_option if (not weechat.config_boolean(fish_config_option['announce']) or fish_encryption_announced.get(target)): return (server, nick) = target.split("/") if (weechat.info_get("irc_is_nick", nick) and weechat.buffer_get_string(buffer, "localvar_type") != "private"): # if we get a private message and there no buffer yet, create one and # jump back to the previous buffer weechat.command(buffer, "/mute -all query %s" % nick) buffer = weechat.info_get("irc_buffer", "%s,%s" % (server, nick)) weechat.command(buffer, "/input jump_previously_visited_buffer") fish_alert(buffer, "Messages to/from %s are encrypted." % target) fish_encryption_announced[target] = True def fish_announce_unencrypted(buffer, target): global fish_encryption_announced, fish_config_option if (not weechat.config_boolean(fish_config_option['announce']) or not fish_encryption_announced.get(target)): return fish_alert(buffer, "Messages to/from %s are %s*not*%s encrypted." % ( target, weechat.color(weechat.config_color(fish_config_option["alert"])), weechat.color("chat"))) del fish_encryption_announced[target] def fish_alert(buffer, message): mark = "%s%s%s\t" % ( weechat.color(weechat.config_color(fish_config_option["alert"])), weechat.config_string(fish_config_option["marker"]), weechat.color("chat")) weechat.prnt(buffer, "%s%s" % (mark, message)) def fish_list_keys(buffer): global fish_keys weechat.prnt(buffer, "\tFiSH Keys: form target(server): key") for (target, key) in sorted(fish_keys.items()): (server, nick) = target.split("/") weechat.prnt(buffer, "\t%s(%s): %s" % (nick, server, key)) def fish_msg_w_marker(msg): marker = weechat.config_string(fish_config_option["mark_encrypted"]).encode() if weechat.config_string(fish_config_option["mark_position"]) == "end": return b"%s%s" % (msg, marker) elif weechat.config_string(fish_config_option["mark_position"]) == "begin": return b"%s%s" % (marker, msg) else: return msg def fish_msg_wo_marker(msg): marker = weechat.config_string(fish_config_option["mark_encrypted"]) if weechat.config_string(fish_config_option["mark_position"]) == "end": return msg[0:-len(marker)] elif weechat.config_string(fish_config_option["mark_position"]) == "begin": return msg[len(marker):] else: return msg # # MAIN # if (__name__ == "__main__" and import_ok and weechat.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "fish_unload_cb", "")): weechat.hook_command( "blowkey", "Manage FiSH keys", "[list] | set [-server ] [] " "| remove [-server ] " "| exchange [-server ] [] " "| genkey", "Add, change or remove key for target or perform DH1080 key" "exchange with .\n" "Target can be a channel or a nick.\n" "\n" "Without arguments this command lists all keys.\n" "\n" "Examples:\n" "Set the key for a channel: /blowkey set -server freenet #blowfish" " key\n" "Remove the key: /blowkey remove #blowfish\n" "Set the key for a query: /blowkey set nick secret+key\n" "List all keys: /blowkey\n" "DH1080: /blowkey exchange nick\n" "\nPlease read the source for a note about DH1080 key exchange\n", "list || set %(irc_channel)|%(nicks)|-server %(irc_servers) %- " "|| remove %(irc_channel)|%(nicks)|-server %(irc_servers) %- " "|| exchange %(nick)|-server %(irc_servers) %-" "|| genkey", "fish_cmd_blowkey", "") fish_config_init() fish_config_read() fish_secure() weechat.hook_modifier("irc_in_notice", "fish_modifier_in_notice_cb", "") weechat.hook_modifier("irc_in_privmsg", "fish_modifier_in_privmsg_cb", "") weechat.hook_modifier("irc_in_topic", "fish_modifier_in_topic_cb", "") weechat.hook_modifier("irc_in_332", "fish_modifier_in_332_cb", "") weechat.hook_modifier( "irc_out_privmsg", "fish_modifier_out_privmsg_cb", "") weechat.hook_modifier("irc_out_topic", "fish_modifier_out_topic_cb", "") weechat.hook_modifier( "input_text_for_buffer", "fish_modifier_input_text", "") weechat.hook_config("fish.secure.key", "fish_secure_key_cb", "") elif (__name__ == "__main__" and len(sys.argv) == 3): key = sys.argv[1] msg = sys.argv[2] print(blowcrypt_unpack(msg, Blowfish(key), key)) weechat-scripts/python/zerotab.py0000644000175100017510000000552415112622400016207 0ustar manumanu# -*- coding: utf-8 -*- # # Script Name: Zerotab.py # Script Author: Lucian Adamson # Script License: GPL # Alternate Contact: Freenode IRC nick i686 # # 2013-01-27, Nils Görs : # version 1.5: make script compatible with Python 3.x # 2011-09-20, Nils Görs : # version 1.4: fixed: latest nick from join/part messages were used. # 2010-12-04, Sebastien Helleu : # version 1.3: use tag "nick_xxx" (WeeChat >= 0.3.4 only) # 2010-08-03, Sebastien Helleu : # version 1.2: fix bug with nick prefixes (op/halfop/..) # 2010-08-03, Sebastien Helleu : # version 1.1: fix bug with self nick SCRIPT_NAME='zerotab' SCRIPT_AUTHOR='Lucian Adamson ' SCRIPT_VERSION='1.5' SCRIPT_LICENSE='GPL' SCRIPT_DESC='Will tab complete the last nick in channel without typing anything first. This is good for rapid conversations.' import_ok=True try: import weechat, re except ImportError: print ('This script must be run under WeeChat') print ('You can obtain a copy of WeeChat, for free, at http://www.weechat.org') import_ok=False latest_speaker={} weechat_version=0 def my_completer(data, buffer, command): global latest_speaker str_input = weechat.buffer_get_string(weechat.current_buffer(), "input") if command == "/input complete_next" and str_input == '': nick = latest_speaker.get(buffer, "") if nick != "": weechat.command(buffer, "/input insert " + nick) return weechat.WEECHAT_RC_OK def hook_print_cb(data, buffer, date, tags, displayed, highlight, prefix, message): global latest_speaker alltags = tags.split(',') if 'notify_message' in alltags: nick = None if int(weechat_version) >= 0x00030400: # in version >= 0.3.4, there is a tag "nick_xxx" for each message for tag in alltags: if tag.startswith('nick_'): nick = tag[5:] break else: # in older versions, no tag, so extract nick from printed message # this is working, except for irc actions (/me ...) nick = prefix if re.match('^[@%+~*&!-]', nick): nick = nick[1:] if nick: local_nick = weechat.buffer_get_string(buffer, "localvar_nick") if nick != local_nick: latest_speaker[buffer] = nick return weechat.WEECHAT_RC_OK if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): weechat_version = weechat.info_get("version_number", "") or 0 weechat.hook_print("", "", "", 1, "hook_print_cb", "") weechat.hook_command_run('/input complete*', 'my_completer', '') weechat-scripts/python/emoji_aliases.py0000644000175100017510000016033515112622405017354 0ustar manumanu# -*- coding: utf-8 -*- # @author: Mike Reinhardt # @email: mreinhardt@gmail.com # @license: BSD """Convert emoji aliases to unicode emoji. Primarily intended to support Slack chat over IRC or XMPP. Contains many extra aliases found in other programs as well. See http://www.emoji-cheat-sheet.com/ for supported sites and emoji chart. """ import re import weechat weechat.register( "emoji_aliases", # name "Mike Reinhardt", # author "1.0.4", # version "BSD", # license "Convert emoji aliases to unicode emoji.", # description "", # shutdown function "utf-8") # charset EMOJI_ALIASES = { u':+1:': u'\U0001F44D', u':-1:': u'\U0001F44E', u':100:': u'\U0001F4AF', u':1234:': u'\U0001F522', u':8ball:': u'\U0001F3B1', u':a:': u'\U0001F170', u':ab:': u'\U0001F18E', u':abc:': u'\U0001F524', u':abcd:': u'\U0001F521', u':accept:': u'\U0001F251', u':admission_tickets:': u'\U0001F39F', u':aerial_tramway:': u'\U0001F6A1', u':airplane:': u'\U00002708', u':airplane_arriving:': u'\U0001F6EC', u':airplane_departure:': u'\U0001F6EB', u':alarm_clock:': u'\U000023F0', u':alembic:': u'\U00002697', u':alien:': u'\U0001F47D', u':alien_monster:': u'\U0001F47E', u':ambulance:': u'\U0001F691', u':american_football:': u'\U0001F3C8', u':amphora:': u'\U0001F3FA', u':anchor:': u'\U00002693', u':angel:': u'\U0001F47C', u':anger:': u'\U0001F4A2', u':anger_symbol:': u'\U0001F4A2', u':angry:': u'\U0001F620', u':angry_face:': u'\U0001F620', u':anguished:': u'\U0001F627', u':anguished_face:': u'\U0001F627', u':ant:': u'\U0001F41C', u':antenna_with_bars:': u'\U0001F4F6', u':apple:': u'\U0001F34E', u':aquarius:': u'\U00002652', u':aries:': u'\U00002648', u':arrow_backward:': u'\U000025C0', u':arrow_double_down:': u'\U000023EC', u':arrow_double_up:': u'\U000023EB', u':arrow_down:': u'\U00002B07', u':arrow_down_small:': u'\U0001F53D', u':arrow_forward:': u'\U000025B6', u':arrow_heading_down:': u'\U00002935', u':arrow_heading_up:': u'\U00002934', u':arrow_left:': u'\U00002B05', u':arrow_lower_left:': u'\U00002199', u':arrow_lower_right:': u'\U00002198', u':arrow_right:': u'\U000027A1', u':arrow_right_hook:': u'\U000021AA', u':arrow_up:': u'\U00002B06', u':arrow_up_down:': u'\U00002195', u':arrow_up_small:': u'\U0001F53C', u':arrow_upper_left:': u'\U00002196', u':arrow_upper_right:': u'\U00002197', u':arrows_clockwise:': u'\U0001F503', u':arrows_counterclockwise:': u'\U0001F504', u':art:': u'\U0001F3A8', u':articulated_lorry:': u'\U0001F69B', u':artist_palette:': u'\U0001F3A8', u':astonished:': u'\U0001F632', u':astonished_face:': u'\U0001F632', u':athletic_shoe:': u'\U0001F45F', u':atm:': u'\U0001F3E7', u':atom_symbol:': u'\U0000269B', u':aubergine:': u'\U0001F346', u':automated_teller_machine:': u'\U0001F3E7', u':automobile:': u'\U0001F697', u':b:': u'\U0001F171', u':baby:': u'\U0001F476', u':baby_angel:': u'\U0001F47C', u':baby_bottle:': u'\U0001F37C', u':baby_chick:': u'\U0001F424', u':baby_symbol:': u'\U0001F6BC', u':back:': u'\U0001F519', u':back_with_leftwards_arrow_above:': u'\U0001F519', u':bactrian_camel:': u'\U0001F42B', u':badminton_racquet_and_shuttlecock:': u'\U0001F3F8', u':baggage_claim:': u'\U0001F6C4', u':balloon:': u'\U0001F388', u':ballot_box_with_ballot:': u'\U0001F5F3', u':ballot_box_with_check:': u'\U00002611', u':bamboo:': u'\U0001F38D', u':banana:': u'\U0001F34C', u':bangbang:': u'\U0000203C', u':bank:': u'\U0001F3E6', u':banknote_with_dollar_sign:': u'\U0001F4B5', u':banknote_with_euro_sign:': u'\U0001F4B6', u':banknote_with_pound_sign:': u'\U0001F4B7', u':banknote_with_yen_sign:': u'\U0001F4B4', u':bar_chart:': u'\U0001F4CA', u':barber:': u'\U0001F488', u':barber_pole:': u'\U0001F488', u':barely_sunny:': u'\U0001F325', u':baseball:': u'\U000026BE', u':basketball:': u'\U0001F3C0', u':basketball_and_hoop:': u'\U0001F3C0', u':bath:': u'\U0001F6C0', u':bathtub:': u'\U0001F6C1', u':battery:': u'\U0001F50B', u':beach_with_umbrella:': u'\U0001F3D6', u':bear:': u'\U0001F43B', u':bear_face:': u'\U0001F43B', u':beating_heart:': u'\U0001F493', u':bed:': u'\U0001F6CF', u':bee:': u'\U0001F41D', u':beer:': u'\U0001F37A', u':beer_mug:': u'\U0001F37A', u':beers:': u'\U0001F37B', u':beetle:': u'\U0001F41E', u':beginner:': u'\U0001F530', u':bell:': u'\U0001F514', u':bell_with_cancellation_stroke:': u'\U0001F515', u':bellhop_bell:': u'\U0001F6CE', u':bento:': u'\U0001F371', u':bento_box:': u'\U0001F371', u':bicycle:': u'\U0001F6B2', u':bicyclist:': u'\U0001F6B4', u':bike:': u'\U0001F6B2', u':bikini:': u'\U0001F459', u':billiards:': u'\U0001F3B1', u':biohazard_sign:': u'\U00002623', u':bird:': u'\U0001F426', u':birthday:': u'\U0001F382', u':birthday_cake:': u'\U0001F382', u':black_circle:': u'\U000026AB', u':black_circle_for_record:': u'\U000023FA', u':black_club_suit:': u'\U00002663', u':black_diamond_suit:': u'\U00002666', u':black_down-pointing_double_triangle:': u'\U000023EC', u':black_heart_suit:': u'\U00002665', u':black_joker:': u'\U0001F0CF', u':black_large_square:': u'\U00002B1B', u':black_left-pointing_double_triangle:': u'\U000023EA', u':black_left-pointing_triangle:': u'\U000025C0', u':black_medium_small_square:': u'\U000025FE', u':black_medium_square:': u'\U000025FC', u':black_nib:': u'\U00002712', u':black_question_mark_ornament:': u'\U00002753', u':black_right-pointing_double_triangle:': u'\U000023E9', u':black_right-pointing_triangle:': u'\U000025B6', u':black_rightwards_arrow:': u'\U000027A1', u':black_scissors:': u'\U00002702', u':black_small_square:': u'\U000025AA', u':black_spade_suit:': u'\U00002660', u':black_square_button:': u'\U0001F532', u':black_square_for_stop:': u'\U000023F9', u':black_sun_with_rays:': u'\U00002600', u':black_telephone:': u'\U0000260E', u':black_universal_recycling_symbol:': u'\U0000267B', u':black_up-pointing_double_triangle:': u'\U000023EB', u':blossom:': u'\U0001F33C', u':blowfish:': u'\U0001F421', u':blue_book:': u'\U0001F4D8', u':blue_car:': u'\U0001F699', u':blue_heart:': u'\U0001F499', u':blush:': u'\U0001F60A', u':boar:': u'\U0001F417', u':boat:': u'\U000026F5', u':bomb:': u'\U0001F4A3', u':book:': u'\U0001F4D6', u':bookmark:': u'\U0001F516', u':bookmark_tabs:': u'\U0001F4D1', u':books:': u'\U0001F4DA', u':boom:': u'\U0001F4A5', u':boot:': u'\U0001F462', u':bottle_with_popping_cork:': u'\U0001F37E', u':bouquet:': u'\U0001F490', u':bow:': u'\U0001F647', u':bow_and_arrow:': u'\U0001F3F9', u':bowling:': u'\U0001F3B3', u':boy:': u'\U0001F466', u':bread:': u'\U0001F35E', u':bride_with_veil:': u'\U0001F470', u':bridge_at_night:': u'\U0001F309', u':briefcase:': u'\U0001F4BC', u':broken_heart:': u'\U0001F494', u':bug:': u'\U0001F41B', u':building_construction:': u'\U0001F3D7', u':bulb:': u'\U0001F4A1', u':bullettrain_front:': u'\U0001F685', u':bullettrain_side:': u'\U0001F684', u':burrito:': u'\U0001F32F', u':bus:': u'\U0001F68C', u':bus_stop:': u'\U0001F68F', u':busstop:': u'\U0001F68F', u':bust_in_silhouette:': u'\U0001F464', u':busts_in_silhouette:': u'\U0001F465', u':cactus:': u'\U0001F335', u':cake:': u'\U0001F370', u':calendar:': u'\U0001F4C5', u':calendar:': u'\U0001F4C6', u':calling:': u'\U0001F4F2', u':camel:': u'\U0001F42B', u':camera:': u'\U0001F4F7', u':camera_with_flash:': u'\U0001F4F8', u':camping:': u'\U0001F3D5', u':cancer:': u'\U0000264B', u':candle:': u'\U0001F56F', u':candy:': u'\U0001F36C', u':capital_abcd:': u'\U0001F520', u':capricorn:': u'\U00002651', u':car:': u'\U0001F697', u':card_file_box:': u'\U0001F5C3', u':card_index:': u'\U0001F4C7', u':card_index_dividers:': u'\U0001F5C2', u':carousel_horse:': u'\U0001F3A0', u':carp_streamer:': u'\U0001F38F', u':cat2:': u'\U0001F408', u':cat:': u'\U0001F408', u':cat:': u'\U0001F431', u':cat_face:': u'\U0001F431', u':cat_face_with_tears_of_joy:': u'\U0001F639', u':cat_face_with_wry_smile:': u'\U0001F63C', u':cd:': u'\U0001F4BF', u':chains:': u'\U000026D3', u':champagne:': u'\U0001F37E', u':chart:': u'\U0001F4B9', u':chart_with_downwards_trend:': u'\U0001F4C9', u':chart_with_upwards_trend:': u'\U0001F4C8', u':chart_with_upwards_trend_and_yen_sign:': u'\U0001F4B9', u':checkered_flag:': u'\U0001F3C1', u':cheering_megaphone:': u'\U0001F4E3', u':cheese_wedge:': u'\U0001F9C0', u':chequered_flag:': u'\U0001F3C1', u':cherries:': u'\U0001F352', u':cherry_blossom:': u'\U0001F338', u':chestnut:': u'\U0001F330', u':chicken:': u'\U0001F414', u':children_crossing:': u'\U0001F6B8', u':chipmunk:': u'\U0001F43F', u':chocolate_bar:': u'\U0001F36B', u':christmas_tree:': u'\U0001F384', u':church:': u'\U000026EA', u':cinema:': u'\U0001F3A6', u':circled_ideograph_accept:': u'\U0001F251', u':circled_ideograph_advantage:': u'\U0001F250', u':circled_ideograph_congratulation:': u'\U00003297', u':circled_ideograph_secret:': u'\U00003299', u':circled_latin_capital_letter_m:': u'\U000024C2', u':circus_tent:': u'\U0001F3AA', u':city_sunrise:': u'\U0001F307', u':city_sunset:': u'\U0001F306', u':cityscape:': u'\U0001F3D9', u':cityscape_at_dusk:': u'\U0001F306', u':cl:': u'\U0001F191', u':clap:': u'\U0001F44F', u':clapper:': u'\U0001F3AC', u':clapper_board:': u'\U0001F3AC', u':clapping_hands_sign:': u'\U0001F44F', u':classical_building:': u'\U0001F3DB', u':clinking_beer_mugs:': u'\U0001F37B', u':clipboard:': u'\U0001F4CB', u':clock1030:': u'\U0001F565', u':clock10:': u'\U0001F559', u':clock1130:': u'\U0001F566', u':clock11:': u'\U0001F55A', u':clock1230:': u'\U0001F567', u':clock12:': u'\U0001F55B', u':clock130:': u'\U0001F55C', u':clock1:': u'\U0001F550', u':clock230:': u'\U0001F55D', u':clock2:': u'\U0001F551', u':clock330:': u'\U0001F55E', u':clock3:': u'\U0001F552', u':clock430:': u'\U0001F55F', u':clock4:': u'\U0001F553', u':clock530:': u'\U0001F560', u':clock5:': u'\U0001F554', u':clock630:': u'\U0001F561', u':clock6:': u'\U0001F555', u':clock730:': u'\U0001F562', u':clock7:': u'\U0001F556', u':clock830:': u'\U0001F563', u':clock8:': u'\U0001F557', u':clock930:': u'\U0001F564', u':clock9:': u'\U0001F558', u':clock_face_eight-thirty:': u'\U0001F563', u':clock_face_eight_oclock:': u'\U0001F557', u':clock_face_eleven-thirty:': u'\U0001F566', u':clock_face_eleven_oclock:': u'\U0001F55A', u':clock_face_five-thirty:': u'\U0001F560', u':clock_face_five_oclock:': u'\U0001F554', u':clock_face_four-thirty:': u'\U0001F55F', u':clock_face_four_oclock:': u'\U0001F553', u':clock_face_nine-thirty:': u'\U0001F564', u':clock_face_nine_oclock:': u'\U0001F558', u':clock_face_one-thirty:': u'\U0001F55C', u':clock_face_one_oclock:': u'\U0001F550', u':clock_face_seven-thirty:': u'\U0001F562', u':clock_face_seven_oclock:': u'\U0001F556', u':clock_face_six-thirty:': u'\U0001F561', u':clock_face_six_oclock:': u'\U0001F555', u':clock_face_ten-thirty:': u'\U0001F565', u':clock_face_ten_oclock:': u'\U0001F559', u':clock_face_three-thirty:': u'\U0001F55E', u':clock_face_three_oclock:': u'\U0001F552', u':clock_face_twelve-thirty:': u'\U0001F567', u':clock_face_twelve_oclock:': u'\U0001F55B', u':clock_face_two-thirty:': u'\U0001F55D', u':clock_face_two_oclock:': u'\U0001F551', u':closed_book:': u'\U0001F4D5', u':closed_lock_with_key:': u'\U0001F510', u':closed_mailbox_with_lowered_flag:': u'\U0001F4EA', u':closed_mailbox_with_raised_flag:': u'\U0001F4EB', u':closed_umbrella:': u'\U0001F302', u':cloud:': u'\U00002601', u':cloud_with_lightning:': u'\U0001F329', u':cloud_with_rain:': u'\U0001F327', u':cloud_with_snow:': u'\U0001F328', u':cloud_with_tornado:': u'\U0001F32A', u':clubs:': u'\U00002663', u':cocktail:': u'\U0001F378', u':cocktail_glass:': u'\U0001F378', u':coffee:': u'\U00002615', u':coffin:': u'\U000026B0', u':cold_sweat:': u'\U0001F630', u':collision:': u'\U0001F4A5', u':collision_symbol:': u'\U0001F4A5', u':comet:': u'\U00002604', u':compression:': u'\U0001F5DC', u':computer:': u'\U0001F4BB', u':confetti_ball:': u'\U0001F38A', u':confounded:': u'\U0001F616', u':confounded_face:': u'\U0001F616', u':confused:': u'\U0001F615', u':confused_face:': u'\U0001F615', u':congratulations:': u'\U00003297', u':construction:': u'\U0001F6A7', u':construction_sign:': u'\U0001F6A7', u':construction_worker:': u'\U0001F477', u':control_knobs:': u'\U0001F39B', u':convenience_store:': u'\U0001F3EA', u':cooked_rice:': u'\U0001F35A', u':cookie:': u'\U0001F36A', u':cooking:': u'\U0001F373', u':cool:': u'\U0001F192', u':cop:': u'\U0001F46E', u':copyright:': u'\U000000A9', u':copyright_sign:': u'\U000000A9', u':corn:': u'\U0001F33D', u':couch_and_lamp:': u'\U0001F6CB', u':couple:': u'\U0001F46B', u':couple_with_heart:': u'\U0001F491', u':couplekiss:': u'\U0001F48F', u':cow2:': u'\U0001F404', u':cow:': u'\U0001F404', u':cow:': u'\U0001F42E', u':cow_face:': u'\U0001F42E', u':crab:': u'\U0001F980', u':credit_card:': u'\U0001F4B3', u':crescent_moon:': u'\U0001F319', u':cricket_bat_and_ball:': u'\U0001F3CF', u':crocodile:': u'\U0001F40A', u':cross_mark:': u'\U0000274C', u':crossed_flags:': u'\U0001F38C', u':crossed_swords:': u'\U00002694', u':crown:': u'\U0001F451', u':cry:': u'\U0001F622', u':crying_cat_face:': u'\U0001F63F', u':crying_face:': u'\U0001F622', u':crystal_ball:': u'\U0001F52E', u':cupid:': u'\U0001F498', u':curly_loop:': u'\U000027B0', u':currency_exchange:': u'\U0001F4B1', u':curry:': u'\U0001F35B', u':curry_and_rice:': u'\U0001F35B', u':custard:': u'\U0001F36E', u':customs:': u'\U0001F6C3', u':cyclone:': u'\U0001F300', u':dagger_knife:': u'\U0001F5E1', u':dancer:': u'\U0001F483', u':dancers:': u'\U0001F46F', u':dango:': u'\U0001F361', u':dark_sunglasses:': u'\U0001F576', u':dart:': u'\U0001F3AF', u':dash:': u'\U0001F4A8', u':dash_symbol:': u'\U0001F4A8', u':date:': u'\U0001F4C5', u':deciduous_tree:': u'\U0001F333', u':delivery_truck:': u'\U0001F69A', u':department_store:': u'\U0001F3EC', u':derelict_house_building:': u'\U0001F3DA', u':desert:': u'\U0001F3DC', u':desert_island:': u'\U0001F3DD', u':desktop_computer:': u'\U0001F5A5', u':diamond_shape_with_a_dot_inside:': u'\U0001F4A0', u':diamonds:': u'\U00002666', u':direct_hit:': u'\U0001F3AF', u':disappointed:': u'\U0001F61E', u':disappointed_but_relieved_face:': u'\U0001F625', u':disappointed_face:': u'\U0001F61E', u':disappointed_relieved:': u'\U0001F625', u':dizzy:': u'\U0001F4AB', u':dizzy_face:': u'\U0001F635', u':dizzy_symbol:': u'\U0001F4AB', u':do_not_litter:': u'\U0001F6AF', u':do_not_litter_symbol:': u'\U0001F6AF', u':dog2:': u'\U0001F415', u':dog:': u'\U0001F415', u':dog:': u'\U0001F436', u':dog_face:': u'\U0001F436', u':dollar:': u'\U0001F4B5', u':dolls:': u'\U0001F38E', u':dolphin:': u'\U0001F42C', u':door:': u'\U0001F6AA', u':double_curly_loop:': u'\U000027BF', u':double_exclamation_mark:': u'\U0000203C', u':double_vertical_bar:': u'\U000023F8', u':doughnut:': u'\U0001F369', u':dove_of_peace:': u'\U0001F54A', u':down-pointing_red_triangle:': u'\U0001F53B', u':down-pointing_small_red_triangle:': u'\U0001F53D', u':downwards_black_arrow:': u'\U00002B07', u':dragon:': u'\U0001F409', u':dragon_face:': u'\U0001F432', u':dress:': u'\U0001F457', u':dromedary_camel:': u'\U0001F42A', u':droplet:': u'\U0001F4A7', u':dvd:': u'\U0001F4C0', u':e-mail:': u'\U0001F4E7', u':e-mail_symbol:': u'\U0001F4E7', u':ear:': u'\U0001F442', u':ear_of_maize:': u'\U0001F33D', u':ear_of_rice:': u'\U0001F33E', u':earth_africa:': u'\U0001F30D', u':earth_americas:': u'\U0001F30E', u':earth_asia:': u'\U0001F30F', u':earth_globe_americas:': u'\U0001F30E', u':earth_globe_asia-australia:': u'\U0001F30F', u':earth_globe_europe-africa:': u'\U0001F30D', u':egg:': u'\U0001F373', u':eggplant:': u'\U0001F346', u':eight_pointed_black_star:': u'\U00002734', u':eight_spoked_asterisk:': u'\U00002733', u':eject_symbol:': u'\U000023CF', u':electric_light_bulb:': u'\U0001F4A1', u':electric_plug:': u'\U0001F50C', u':electric_torch:': u'\U0001F526', u':elephant:': u'\U0001F418', u':email:': u'\U00002709', u':emoji_modifier_fitzpatrick_type-1-2:': u'\U0001F3FB', u':emoji_modifier_fitzpatrick_type-3:': u'\U0001F3FC', u':emoji_modifier_fitzpatrick_type-4:': u'\U0001F3FD', u':emoji_modifier_fitzpatrick_type-5:': u'\U0001F3FE', u':emoji_modifier_fitzpatrick_type-6:': u'\U0001F3FF', u':end:': u'\U0001F51A', u':end_with_leftwards_arrow_above:': u'\U0001F51A', u':envelope:': u'\U00002709', u':envelope_with_arrow:': u'\U0001F4E9', u':envelope_with_downwards_arrow_above:': u'\U0001F4E9', u':euro:': u'\U0001F4B6', u':european_castle:': u'\U0001F3F0', u':european_post_office:': u'\U0001F3E4', u':evergreen_tree:': u'\U0001F332', u':exclamation:': u'\U00002757', u':exclamation_question_mark:': u'\U00002049', u':expressionless:': u'\U0001F611', u':expressionless_face:': u'\U0001F611', u':extraterrestrial_alien:': u'\U0001F47D', u':eye:': u'\U0001F441', u':eyeglasses:': u'\U0001F453', u':eyes:': u'\U0001F440', u':face_massage:': u'\U0001F486', u':face_savouring_delicious_food:': u'\U0001F60B', u':face_screaming_in_fear:': u'\U0001F631', u':face_throwing_a_kiss:': u'\U0001F618', u':face_with_cold_sweat:': u'\U0001F613', u':face_with_head-bandage:': u'\U0001F915', u':face_with_head_bandage:': u'\U0001F915', u':face_with_look_of_triumph:': u'\U0001F624', u':face_with_medical_mask:': u'\U0001F637', u':face_with_no_good_gesture:': u'\U0001F645', u':face_with_ok_gesture:': u'\U0001F646', u':face_with_open_mouth:': u'\U0001F62E', u':face_with_open_mouth_and_cold_sweat:': u'\U0001F630', u':face_with_rolling_eyes:': u'\U0001F644', u':face_with_stuck-out_tongue:': u'\U0001F61B', u':face_with_tears_of_joy:': u'\U0001F602', u':face_with_thermometer:': u'\U0001F912', u':face_without_mouth:': u'\U0001F636', u':facepunch:': u'\U0001F44A', u':factory:': u'\U0001F3ED', u':fallen_leaf:': u'\U0001F342', u':family:': u'\U0001F46A', u':fast_forward:': u'\U000023E9', u':father_christmas:': u'\U0001F385', u':fax:': u'\U0001F4E0', u':fax_machine:': u'\U0001F4E0', u':fearful:': u'\U0001F628', u':fearful_face:': u'\U0001F628', u':feet:': u'\U0001F43E', u':ferris_wheel:': u'\U0001F3A1', u':ferry:': u'\U000026F4', u':field_hockey_stick_and_ball:': u'\U0001F3D1', u':file_cabinet:': u'\U0001F5C4', u':file_folder:': u'\U0001F4C1', u':film_frames:': u'\U0001F39E', u':film_projector:': u'\U0001F4FD', u':fire:': u'\U0001F525', u':fire_engine:': u'\U0001F692', u':firework_sparkler:': u'\U0001F387', u':fireworks:': u'\U0001F386', u':first_quarter_moon:': u'\U0001F313', u':first_quarter_moon_symbol:': u'\U0001F313', u':first_quarter_moon_with_face:': u'\U0001F31B', u':fish:': u'\U0001F41F', u':fish_cake:': u'\U0001F365', u':fish_cake_with_swirl_design:': u'\U0001F365', u':fishing_pole_and_fish:': u'\U0001F3A3', u':fist:': u'\U0000270A', u':fisted_hand_sign:': u'\U0001F44A', u':flag_in_hole:': u'\U000026F3', u':flags:': u'\U0001F38F', u':flashlight:': u'\U0001F526', u':fleur-de-lis:': u'\U0000269C', u':fleur_de_lis:': u'\U0000269C', u':flexed_biceps:': u'\U0001F4AA', u':flipper:': u'\U0001F42C', u':floppy_disk:': u'\U0001F4BE', u':flower_playing_cards:': u'\U0001F3B4', u':flushed:': u'\U0001F633', u':flushed_face:': u'\U0001F633', u':fog:': u'\U0001F32B', u':foggy:': u'\U0001F301', u':football:': u'\U0001F3C8', u':footprints:': u'\U0001F463', u':fork_and_knife:': u'\U0001F374', u':fork_and_knife_with_plate:': u'\U0001F37D', u':fountain:': u'\U000026F2', u':four_leaf_clover:': u'\U0001F340', u':frame_with_picture:': u'\U0001F5BC', u':free:': u'\U0001F193', u':french_fries:': u'\U0001F35F', u':fried_shrimp:': u'\U0001F364', u':fries:': u'\U0001F35F', u':frog:': u'\U0001F438', u':frog_face:': u'\U0001F438', u':front-facing_baby_chick:': u'\U0001F425', u':frowning:': u'\U0001F626', u':frowning_face_with_open_mouth:': u'\U0001F626', u':fuel_pump:': u'\U000026FD', u':fuelpump:': u'\U000026FD', u':full_moon:': u'\U0001F315', u':full_moon_symbol:': u'\U0001F315', u':full_moon_with_face:': u'\U0001F31D', u':funeral_urn:': u'\U000026B1', u':game_die:': u'\U0001F3B2', u':gear:': u'\U00002699', u':gem:': u'\U0001F48E', u':gem_stone:': u'\U0001F48E', u':gemini:': u'\U0000264A', u':ghost:': u'\U0001F47B', u':gift:': u'\U0001F381', u':gift_heart:': u'\U0001F49D', u':girl:': u'\U0001F467', u':globe_with_meridians:': u'\U0001F310', u':glowing_star:': u'\U0001F31F', u':goat:': u'\U0001F410', u':golf:': u'\U000026F3', u':golfer:': u'\U0001F3CC', u':graduation_cap:': u'\U0001F393', u':grapes:': u'\U0001F347', u':green_apple:': u'\U0001F34F', u':green_book:': u'\U0001F4D7', u':green_heart:': u'\U0001F49A', u':grey_exclamation:': u'\U00002755', u':grey_question:': u'\U00002754', u':grimacing:': u'\U0001F62C', u':grimacing_face:': u'\U0001F62C', u':grin:': u'\U0001F601', u':grinning:': u'\U0001F600', u':grinning_cat_face_with_smiling_eyes:': u'\U0001F638', u':grinning_face:': u'\U0001F600', u':grinning_face_with_smiling_eyes:': u'\U0001F601', u':growing_heart:': u'\U0001F497', u':guardsman:': u'\U0001F482', u':guitar:': u'\U0001F3B8', u':gun:': u'\U0001F52B', u':haircut:': u'\U0001F487', u':hamburger:': u'\U0001F354', u':hammer:': u'\U0001F528', u':hammer_and_pick:': u'\U00002692', u':hammer_and_wrench:': u'\U0001F6E0', u':hamster:': u'\U0001F439', u':hamster_face:': u'\U0001F439', u':hand:': u'\U0000270B', u':handbag:': u'\U0001F45C', u':hankey:': u'\U0001F4A9', u':happy_person_raising_one_hand:': u'\U0001F64B', u':hatched_chick:': u'\U0001F425', u':hatching_chick:': u'\U0001F423', u':headphone:': u'\U0001F3A7', u':headphones:': u'\U0001F3A7', u':hear-no-evil_monkey:': u'\U0001F649', u':hear_no_evil:': u'\U0001F649', u':heart:': u'\U00002764', u':heart_decoration:': u'\U0001F49F', u':heart_eyes:': u'\U0001F60D', u':heart_eyes_cat:': u'\U0001F63B', u':heart_with_arrow:': u'\U0001F498', u':heart_with_ribbon:': u'\U0001F49D', u':heartbeat:': u'\U0001F493', u':heartpulse:': u'\U0001F497', u':hearts:': u'\U00002665', u':heavy_black_heart:': u'\U00002764', u':heavy_check_mark:': u'\U00002714', u':heavy_division_sign:': u'\U00002797', u':heavy_dollar_sign:': u'\U0001F4B2', u':heavy_exclamation_mark:': u'\U00002757', u':heavy_exclamation_mark_symbol:': u'\U00002757', u':heavy_heart_exclamation_mark_ornament:': u'\U00002763', u':heavy_large_circle:': u'\U00002B55', u':heavy_minus_sign:': u'\U00002796', u':heavy_multiplication_x:': u'\U00002716', u':heavy_plus_sign:': u'\U00002795', u':helicopter:': u'\U0001F681', u':helm_symbol:': u'\U00002388', u':helmet_with_white_cross:': u'\U000026D1', u':herb:': u'\U0001F33F', u':hibiscus:': u'\U0001F33A', u':high-heeled_shoe:': u'\U0001F460', u':high-speed_train:': u'\U0001F684', u':high-speed_train_with_bullet_nose:': u'\U0001F685', u':high_brightness:': u'\U0001F506', u':high_brightness_symbol:': u'\U0001F506', u':high_heel:': u'\U0001F460', u':high_voltage_sign:': u'\U000026A1', u':hocho:': u'\U0001F52A', u':hole:': u'\U0001F573', u':honey_pot:': u'\U0001F36F', u':honeybee:': u'\U0001F41D', u':horizontal_traffic_light:': u'\U0001F6A5', u':horse:': u'\U0001F40E', u':horse:': u'\U0001F434', u':horse_face:': u'\U0001F434', u':horse_racing:': u'\U0001F3C7', u':hospital:': u'\U0001F3E5', u':hot_beverage:': u'\U00002615', u':hot_dog:': u'\U0001F32D', u':hot_pepper:': u'\U0001F336', u':hot_springs:': u'\U00002668', u':hotdog:': u'\U0001F32D', u':hotel:': u'\U0001F3E8', u':hotsprings:': u'\U00002668', u':hourglass:': u'\U0000231B', u':hourglass_flowing_sand:': u'\U000023F3', u':hourglass_with_flowing_sand:': u'\U000023F3', u':house:': u'\U0001F3E0', u':house_building:': u'\U0001F3E0', u':house_buildings:': u'\U0001F3D8', u':house_with_garden:': u'\U0001F3E1', u':hugging_face:': u'\U0001F917', u':hundred_points_symbol:': u'\U0001F4AF', u':hushed:': u'\U0001F62F', u':hushed_face:': u'\U0001F62F', u':ice_cream:': u'\U0001F368', u':ice_hockey_stick_and_puck:': u'\U0001F3D2', u':ice_skate:': u'\U000026F8', u':icecream:': u'\U0001F366', u':id:': u'\U0001F194', u':ideograph_advantage:': u'\U0001F250', u':imp:': u'\U0001F47F', u':inbox_tray:': u'\U0001F4E5', u':incoming_envelope:': u'\U0001F4E8', u':information_desk_person:': u'\U0001F481', u':information_source:': u'\U00002139', u':innocent:': u'\U0001F607', u':input_symbol_for_latin_capital_letters:': u'\U0001F520', u':input_symbol_for_latin_letters:': u'\U0001F524', u':input_symbol_for_latin_small_letters:': u'\U0001F521', u':input_symbol_for_numbers:': u'\U0001F522', u':input_symbol_for_symbols:': u'\U0001F523', u':interrobang:': u'\U00002049', u':iphone:': u'\U0001F4F1', u':izakaya_lantern:': u'\U0001F3EE', u':jack-o-lantern:': u'\U0001F383', u':jack_o_lantern:': u'\U0001F383', u':japan:': u'\U0001F5FE', u':japanese_castle:': u'\U0001F3EF', u':japanese_dolls:': u'\U0001F38E', u':japanese_goblin:': u'\U0001F47A', u':japanese_ogre:': u'\U0001F479', u':japanese_post_office:': u'\U0001F3E3', u':japanese_symbol_for_beginner:': u'\U0001F530', u':jeans:': u'\U0001F456', u':joy:': u'\U0001F602', u':joy_cat:': u'\U0001F639', u':joystick:': u'\U0001F579', u':kaaba:': u'\U0001F54B', u':key:': u'\U0001F511', u':keyboard:': u'\U00002328', u':keycap_ten:': u'\U0001F51F', u':kimono:': u'\U0001F458', u':kiss:': u'\U0001F48B', u':kiss:': u'\U0001F48F', u':kiss_mark:': u'\U0001F48B', u':kissing:': u'\U0001F617', u':kissing_cat:': u'\U0001F63D', u':kissing_cat_face_with_closed_eyes:': u'\U0001F63D', u':kissing_closed_eyes:': u'\U0001F61A', u':kissing_face:': u'\U0001F617', u':kissing_face_with_closed_eyes:': u'\U0001F61A', u':kissing_face_with_smiling_eyes:': u'\U0001F619', u':kissing_heart:': u'\U0001F618', u':kissing_smiling_eyes:': u'\U0001F619', u':knife:': u'\U0001F52A', u':knife_fork_plate:': u'\U0001F37D', u':koala:': u'\U0001F428', u':koko:': u'\U0001F201', u':label:': u'\U0001F3F7', u':lady_beetle:': u'\U0001F41E', u':lantern:': u'\U0001F3EE', u':large_blue_circle:': u'\U0001F535', u':large_blue_diamond:': u'\U0001F537', u':large_orange_diamond:': u'\U0001F536', u':large_red_circle:': u'\U0001F534', u':last_quarter_moon:': u'\U0001F317', u':last_quarter_moon_symbol:': u'\U0001F317', u':last_quarter_moon_with_face:': u'\U0001F31C', u':latin_cross:': u'\U0000271D', u':laughing:': u'\U0001F606', u':leaf_fluttering_in_wind:': u'\U0001F343', u':leaves:': u'\U0001F343', u':ledger:': u'\U0001F4D2', u':left-pointing_magnifying_glass:': u'\U0001F50D', u':left_luggage:': u'\U0001F6C5', u':left_right_arrow:': u'\U00002194', u':left_speech_bubble:': u'\U0001F4AC', u':leftwards_arrow_with_hook:': u'\U000021A9', u':leftwards_black_arrow:': u'\U00002B05', u':lemon:': u'\U0001F34B', u':leo:': u'\U0000264C', u':leopard:': u'\U0001F406', u':level_slider:': u'\U0001F39A', u':libra:': u'\U0000264E', u':light_rail:': u'\U0001F688', u':lightning:': u'\U0001F329', u':link:': u'\U0001F517', u':link_symbol:': u'\U0001F517', u':linked_paperclips:': u'\U0001F587', u':lion_face:': u'\U0001F981', u':lips:': u'\U0001F444', u':lipstick:': u'\U0001F484', u':lock:': u'\U0001F512', u':lock_with_ink_pen:': u'\U0001F50F', u':lollipop:': u'\U0001F36D', u':loop:': u'\U000027BF', u':loud_sound:': u'\U0001F50A', u':loudly_crying_face:': u'\U0001F62D', u':loudspeaker:': u'\U0001F4E2', u':love_hotel:': u'\U0001F3E9', u':love_letter:': u'\U0001F48C', u':low_brightness:': u'\U0001F505', u':low_brightness_symbol:': u'\U0001F505', u':lower_left_ballpoint_pen:': u'\U0001F58A', u':lower_left_crayon:': u'\U0001F58D', u':lower_left_fountain_pen:': u'\U0001F58B', u':lower_left_paintbrush:': u'\U0001F58C', u':m:': u'\U000024C2', u':mag:': u'\U0001F50D', u':mag_right:': u'\U0001F50E', u':mahjong:': u'\U0001F004', u':mahjong_tile_red_dragon:': u'\U0001F004', u':mailbox:': u'\U0001F4EB', u':mailbox_closed:': u'\U0001F4EA', u':mailbox_with_mail:': u'\U0001F4EC', u':mailbox_with_no_mail:': u'\U0001F4ED', u':man:': u'\U0001F468', u':man_and_woman_holding_hands:': u'\U0001F46B', u':man_in_business_suit_levitating:': u'\U0001F574', u':man_with_gua_pi_mao:': u'\U0001F472', u':man_with_turban:': u'\U0001F473', u':mans_shoe:': u'\U0001F45E', u':mantelpiece_clock:': u'\U0001F570', u':maple_leaf:': u'\U0001F341', u':mask:': u'\U0001F637', u':massage:': u'\U0001F486', u':meat_on_bone:': u'\U0001F356', u':medal:': u'\U0001F3C5', u':medium_black_circle:': u'\U000026AB', u':medium_white_circle:': u'\U000026AA', u':mega:': u'\U0001F4E3', u':melon:': u'\U0001F348', u':memo:': u'\U0001F4DD', u':menorah_with_nine_branches:': u'\U0001F54E', u':mens:': u'\U0001F6B9', u':mens_symbol:': u'\U0001F6B9', u':metro:': u'\U0001F687', u':microphone:': u'\U0001F3A4', u':microscope:': u'\U0001F52C', u':middle_finger:': u'\U0001F595', u':military_medal:': u'\U0001F396', u':milky_way:': u'\U0001F30C', u':minibus:': u'\U0001F690', u':minidisc:': u'\U0001F4BD', u':mobile_phone:': u'\U0001F4F1', u':mobile_phone_off:': u'\U0001F4F4', u':money-mouth_face:': u'\U0001F911', u':money_bag:': u'\U0001F4B0', u':money_mouth_face:': u'\U0001F911', u':money_with_wings:': u'\U0001F4B8', u':moneybag:': u'\U0001F4B0', u':monkey:': u'\U0001F412', u':monkey_face:': u'\U0001F435', u':monorail:': u'\U0001F69D', u':moon:': u'\U0001F314', u':moon_viewing_ceremony:': u'\U0001F391', u':mortar_board:': u'\U0001F393', u':mosque:': u'\U0001F54C', u':mostly_sunny:': u'\U0001F324', u':motor_boat:': u'\U0001F6E5', u':motorway:': u'\U0001F6E3', u':mount_fuji:': u'\U0001F5FB', u':mountain:': u'\U000026F0', u':mountain_bicyclist:': u'\U0001F6B5', u':mountain_cableway:': u'\U0001F6A0', u':mountain_railway:': u'\U0001F69E', u':mouse2:': u'\U0001F401', u':mouse:': u'\U0001F401', u':mouse:': u'\U0001F42D', u':mouse_face:': u'\U0001F42D', u':mouth:': u'\U0001F444', u':movie_camera:': u'\U0001F3A5', u':moyai:': u'\U0001F5FF', u':multiple_musical_notes:': u'\U0001F3B6', u':muscle:': u'\U0001F4AA', u':mushroom:': u'\U0001F344', u':musical_keyboard:': u'\U0001F3B9', u':musical_note:': u'\U0001F3B5', u':musical_score:': u'\U0001F3BC', u':mute:': u'\U0001F507', u':nail_care:': u'\U0001F485', u':nail_polish:': u'\U0001F485', u':name_badge:': u'\U0001F4DB', u':national_park:': u'\U0001F3DE', u':necktie:': u'\U0001F454', u':negative_squared_ab:': u'\U0001F18E', u':negative_squared_cross_mark:': u'\U0000274E', u':nerd_face:': u'\U0001F913', u':neutral_face:': u'\U0001F610', u':new:': u'\U0001F195', u':new_moon:': u'\U0001F311', u':new_moon_symbol:': u'\U0001F311', u':new_moon_with_face:': u'\U0001F31A', u':newspaper:': u'\U0001F4F0', u':ng:': u'\U0001F196', u':night_with_stars:': u'\U0001F303', u':no_bell:': u'\U0001F515', u':no_bicycles:': u'\U0001F6B3', u':no_entry:': u'\U000026D4', u':no_entry_sign:': u'\U0001F6AB', u':no_good:': u'\U0001F645', u':no_mobile_phones:': u'\U0001F4F5', u':no_mouth:': u'\U0001F636', u':no_one_under_eighteen_symbol:': u'\U0001F51E', u':no_pedestrians:': u'\U0001F6B7', u':no_smoking:': u'\U0001F6AD', u':no_smoking_symbol:': u'\U0001F6AD', u':non-potable_water:': u'\U0001F6B1', u':non-potable_water_symbol:': u'\U0001F6B1', u':north_east_arrow:': u'\U00002197', u':north_west_arrow:': u'\U00002196', u':nose:': u'\U0001F443', u':notebook:': u'\U0001F4D3', u':notebook_with_decorative_cover:': u'\U0001F4D4', u':notes:': u'\U0001F3B6', u':nut_and_bolt:': u'\U0001F529', u':o2:': u'\U0001F17E', u':o:': u'\U00002B55', u':ocean:': u'\U0001F30A', u':octopus:': u'\U0001F419', u':oden:': u'\U0001F362', u':office:': u'\U0001F3E2', u':office_building:': u'\U0001F3E2', u':oil_drum:': u'\U0001F6E2', u':ok:': u'\U0001F197', u':ok_hand:': u'\U0001F44C', u':ok_hand_sign:': u'\U0001F44C', u':ok_woman:': u'\U0001F646', u':old_key:': u'\U0001F5DD', u':older_man:': u'\U0001F474', u':older_woman:': u'\U0001F475', u':om_symbol:': u'\U0001F549', u':on:': u'\U0001F51B', u':oncoming_automobile:': u'\U0001F698', u':oncoming_bus:': u'\U0001F68D', u':oncoming_police_car:': u'\U0001F694', u':oncoming_taxi:': u'\U0001F696', u':open_book:': u'\U0001F4D6', u':open_file_folder:': u'\U0001F4C2', u':open_hands:': u'\U0001F450', u':open_hands_sign:': u'\U0001F450', u':open_lock:': u'\U0001F513', u':open_mailbox_with_lowered_flag:': u'\U0001F4ED', u':open_mailbox_with_raised_flag:': u'\U0001F4EC', u':open_mouth:': u'\U0001F62E', u':ophiuchus:': u'\U000026CE', u':optical_disc:': u'\U0001F4BF', u':orange_book:': u'\U0001F4D9', u':orthodox_cross:': u'\U00002626', u':outbox_tray:': u'\U0001F4E4', u':ox:': u'\U0001F402', u':package:': u'\U0001F4E6', u':page_facing_up:': u'\U0001F4C4', u':page_with_curl:': u'\U0001F4C3', u':pager:': u'\U0001F4DF', u':palm_tree:': u'\U0001F334', u':panda_face:': u'\U0001F43C', u':paperclip:': u'\U0001F4CE', u':parking:': u'\U0001F17F', u':part_alternation_mark:': u'\U0000303D', u':partly_sunny:': u'\U000026C5', u':partly_sunny_rain:': u'\U0001F326', u':party_popper:': u'\U0001F389', u':passenger_ship:': u'\U0001F6F3', u':passport_control:': u'\U0001F6C2', u':paw_prints:': u'\U0001F43E', u':peace_symbol:': u'\U0000262E', u':peach:': u'\U0001F351', u':pear:': u'\U0001F350', u':pedestrian:': u'\U0001F6B6', u':pencil2:': u'\U0000270F', u':pencil:': u'\U0000270F', u':pencil:': u'\U0001F4DD', u':penguin:': u'\U0001F427', u':pensive:': u'\U0001F614', u':pensive_face:': u'\U0001F614', u':performing_arts:': u'\U0001F3AD', u':persevere:': u'\U0001F623', u':persevering_face:': u'\U0001F623', u':person_bowing_deeply:': u'\U0001F647', u':person_frowning:': u'\U0001F64D', u':person_with_ball:': u'\U000026F9', u':person_with_blond_hair:': u'\U0001F471', u':person_with_folded_hands:': u'\U0001F64F', u':person_with_pouting_face:': u'\U0001F64E', u':personal_computer:': u'\U0001F4BB', u':phone:': u'\U0000260E', u':pick:': u'\U000026CF', u':pig2:': u'\U0001F416', u':pig:': u'\U0001F416', u':pig:': u'\U0001F437', u':pig_face:': u'\U0001F437', u':pig_nose:': u'\U0001F43D', u':pile_of_poo:': u'\U0001F4A9', u':pill:': u'\U0001F48A', u':pine_decoration:': u'\U0001F38D', u':pineapple:': u'\U0001F34D', u':pisces:': u'\U00002653', u':pistol:': u'\U0001F52B', u':pizza:': u'\U0001F355', u':place_of_worship:': u'\U0001F6D0', u':playing_card_black_joker:': u'\U0001F0CF', u':point_down:': u'\U0001F447', u':point_left:': u'\U0001F448', u':point_right:': u'\U0001F449', u':point_up:': u'\U0000261D', u':point_up_2:': u'\U0001F446', u':police_car:': u'\U0001F693', u':police_cars_revolving_light:': u'\U0001F6A8', u':police_officer:': u'\U0001F46E', u':poodle:': u'\U0001F429', u':poop:': u'\U0001F4A9', u':popcorn:': u'\U0001F37F', u':post_office:': u'\U0001F3E3', u':postal_horn:': u'\U0001F4EF', u':postbox:': u'\U0001F4EE', u':pot_of_food:': u'\U0001F372', u':potable_water:': u'\U0001F6B0', u':potable_water_symbol:': u'\U0001F6B0', u':pouch:': u'\U0001F45D', u':poultry_leg:': u'\U0001F357', u':pound:': u'\U0001F4B7', u':pouting_cat:': u'\U0001F63E', u':pouting_cat_face:': u'\U0001F63E', u':pouting_face:': u'\U0001F621', u':pray:': u'\U0001F64F', u':prayer_beads:': u'\U0001F4FF', u':princess:': u'\U0001F478', u':printer:': u'\U0001F5A8', u':public_address_loudspeaker:': u'\U0001F4E2', u':punch:': u'\U0001F44A', u':purple_heart:': u'\U0001F49C', u':purse:': u'\U0001F45B', u':pushpin:': u'\U0001F4CC', u':put_litter_in_its_place:': u'\U0001F6AE', u':put_litter_in_its_place_symbol:': u'\U0001F6AE', u':question:': u'\U00002753', u':rabbit2:': u'\U0001F407', u':rabbit:': u'\U0001F407', u':rabbit:': u'\U0001F430', u':rabbit_face:': u'\U0001F430', u':racehorse:': u'\U0001F40E', u':racing_car:': u'\U0001F3CE', u':racing_motorcycle:': u'\U0001F3CD', u':radio:': u'\U0001F4FB', u':radio_button:': u'\U0001F518', u':radioactive_sign:': u'\U00002622', u':rage:': u'\U0001F621', u':railway_car:': u'\U0001F683', u':railway_track:': u'\U0001F6E4', u':rain_cloud:': u'\U0001F327', u':rainbow:': u'\U0001F308', u':raised_fist:': u'\U0000270A', u':raised_hand:': u'\U0000270B', u':raised_hand_with_fingers_splayed:': u'\U0001F590', u':raised_hands:': u'\U0001F64C', u':raising_hand:': u'\U0001F64B', u':ram:': u'\U0001F40F', u':ramen:': u'\U0001F35C', u':rat:': u'\U0001F400', u':recreational_vehicle:': u'\U0001F699', u':recycle:': u'\U0000267B', u':red_apple:': u'\U0001F34E', u':red_car:': u'\U0001F697', u':red_circle:': u'\U0001F534', u':registered:': u'\U000000AE', u':registered_sign:': u'\U000000AE', u':relaxed:': u'\U0000263A', u':relieved:': u'\U0001F60C', u':relieved_face:': u'\U0001F60C', u':reminder_ribbon:': u'\U0001F397', u':repeat:': u'\U0001F501', u':repeat_one:': u'\U0001F502', u':restroom:': u'\U0001F6BB', u':revolving_hearts:': u'\U0001F49E', u':rewind:': u'\U000023EA', u':ribbon:': u'\U0001F380', u':rice:': u'\U0001F35A', u':rice_ball:': u'\U0001F359', u':rice_cracker:': u'\U0001F358', u':rice_scene:': u'\U0001F391', u':right-pointing_magnifying_glass:': u'\U0001F50E', u':right_anger_bubble:': u'\U0001F5EF', u':rightwards_arrow_with_hook:': u'\U000021AA', u':ring:': u'\U0001F48D', u':roasted_sweet_potato:': u'\U0001F360', u':robot_face:': u'\U0001F916', u':rocket:': u'\U0001F680', u':rolled-up_newspaper:': u'\U0001F5DE', u':rolled_up_newspaper:': u'\U0001F5DE', u':roller_coaster:': u'\U0001F3A2', u':rooster:': u'\U0001F413', u':rose:': u'\U0001F339', u':rosette:': u'\U0001F3F5', u':rotating_light:': u'\U0001F6A8', u':round_pushpin:': u'\U0001F4CD', u':rowboat:': u'\U0001F6A3', u':rugby_football:': u'\U0001F3C9', u':runner:': u'\U0001F3C3', u':running:': u'\U0001F3C3', u':running_shirt_with_sash:': u'\U0001F3BD', u':sa:': u'\U0001F202', u':sagittarius:': u'\U00002650', u':sailboat:': u'\U000026F5', u':sake:': u'\U0001F376', u':sake_bottle_and_cup:': u'\U0001F376', u':sandal:': u'\U0001F461', u':santa:': u'\U0001F385', u':satellite:': u'\U0001F4E1', u':satellite:': u'\U0001F6F0', u':satellite_antenna:': u'\U0001F4E1', u':satisfied:': u'\U0001F606', u':saxophone:': u'\U0001F3B7', u':scales:': u'\U00002696', u':school:': u'\U0001F3EB', u':school_satchel:': u'\U0001F392', u':scissors:': u'\U00002702', u':scorpion:': u'\U0001F982', u':scorpius:': u'\U0000264F', u':scream:': u'\U0001F631', u':scream_cat:': u'\U0001F640', u':scroll:': u'\U0001F4DC', u':seat:': u'\U0001F4BA', u':secret:': u'\U00003299', u':see-no-evil_monkey:': u'\U0001F648', u':see_no_evil:': u'\U0001F648', u':seedling:': u'\U0001F331', u':shamrock:': u'\U00002618', u':shaved_ice:': u'\U0001F367', u':sheep:': u'\U0001F411', u':shell:': u'\U0001F41A', u':shield:': u'\U0001F6E1', u':shinto_shrine:': u'\U000026E9', u':ship:': u'\U0001F6A2', u':shirt:': u'\U0001F455', u':shit:': u'\U0001F4A9', u':shoe:': u'\U0001F45E', u':shooting_star:': u'\U0001F320', u':shopping_bags:': u'\U0001F6CD', u':shortcake:': u'\U0001F370', u':shower:': u'\U0001F6BF', u':sign_of_the_horns:': u'\U0001F918', u':signal_strength:': u'\U0001F4F6', u':silhouette_of_japan:': u'\U0001F5FE', u':simple_smile:': u'\U0001F642', u':six_pointed_star:': u'\U0001F52F', u':ski:': u'\U0001F3BF', u':ski_and_ski_boot:': u'\U0001F3BF', u':skier:': u'\U000026F7', u':skull:': u'\U0001F480', u':skull_and_crossbones:': u'\U00002620', u':sleeping:': u'\U0001F634', u':sleeping_accommodation:': u'\U0001F6CC', u':sleeping_face:': u'\U0001F634', u':sleeping_symbol:': u'\U0001F4A4', u':sleepy:': u'\U0001F62A', u':sleepy_face:': u'\U0001F62A', u':sleuth_or_spy:': u'\U0001F575', u':slice_of_pizza:': u'\U0001F355', u':slightly_frowning_face:': u'\U0001F641', u':slightly_smiling_face:': u'\U0001F642', u':slot_machine:': u'\U0001F3B0', u':small_airplane:': u'\U0001F6E9', u':small_blue_diamond:': u'\U0001F539', u':small_orange_diamond:': u'\U0001F538', u':small_red_triangle:': u'\U0001F53A', u':small_red_triangle_down:': u'\U0001F53B', u':smile:': u'\U0001F604', u':smile_cat:': u'\U0001F638', u':smiley:': u'\U0001F603', u':smiley_cat:': u'\U0001F63A', u':smiling_cat_face_with_heart-shaped_eyes:': u'\U0001F63B', u':smiling_cat_face_with_open_mouth:': u'\U0001F63A', u':smiling_face_with_halo:': u'\U0001F607', u':smiling_face_with_heart-shaped_eyes:': u'\U0001F60D', u':smiling_face_with_horns:': u'\U0001F608', u':smiling_face_with_open_mouth:': u'\U0001F603', u':smiling_face_with_open_mouth_and_cold_sweat:': u'\U0001F605', u':smiling_face_with_open_mouth_and_smiling_eyes:': u'\U0001F604', u':smiling_face_with_open_mouth_and_tightly-closed_eyes:': u'\U0001F606', u':smiling_face_with_smiling_eyes:': u'\U0001F60A', u':smiling_face_with_sunglasses:': u'\U0001F60E', u':smiling_imp:': u'\U0001F608', u':smirk:': u'\U0001F60F', u':smirk_cat:': u'\U0001F63C', u':smirking_face:': u'\U0001F60F', u':smoking:': u'\U0001F6AC', u':smoking_symbol:': u'\U0001F6AC', u':snail:': u'\U0001F40C', u':snake:': u'\U0001F40D', u':snow_capped_mountain:': u'\U0001F3D4', u':snow_cloud:': u'\U0001F328', u':snowboarder:': u'\U0001F3C2', u':snowflake:': u'\U00002744', u':snowman:': u'\U00002603', u':snowman_without_snow:': u'\U000026C4', u':sob:': u'\U0001F62D', u':soccer:': u'\U000026BD', u':soccer_ball:': u'\U000026BD', u':soft_ice_cream:': u'\U0001F366', u':soon:': u'\U0001F51C', u':soon_with_rightwards_arrow_above:': u'\U0001F51C', u':sos:': u'\U0001F198', u':sound:': u'\U0001F509', u':south_east_arrow:': u'\U00002198', u':south_west_arrow:': u'\U00002199', u':space_invader:': u'\U0001F47E', u':spades:': u'\U00002660', u':spaghetti:': u'\U0001F35D', u':sparkle:': u'\U00002747', u':sparkler:': u'\U0001F387', u':sparkles:': u'\U00002728', u':sparkling_heart:': u'\U0001F496', u':speak-no-evil_monkey:': u'\U0001F64A', u':speak_no_evil:': u'\U0001F64A', u':speaker:': u'\U0001F508', u':speaker_with_cancellation_stroke:': u'\U0001F507', u':speaker_with_one_sound_wave:': u'\U0001F509', u':speaker_with_three_sound_waves:': u'\U0001F50A', u':speaking_head_in_silhouette:': u'\U0001F5E3', u':speech_balloon:': u'\U0001F4AC', u':speedboat:': u'\U0001F6A4', u':spider:': u'\U0001F577', u':spider_web:': u'\U0001F578', u':spiral_calendar_pad:': u'\U0001F5D3', u':spiral_note_pad:': u'\U0001F5D2', u':spiral_shell:': u'\U0001F41A', u':splashing_sweat_symbol:': u'\U0001F4A6', u':spock-hand:': u'\U0001F596', u':spock_hand:': u'\U0001F596', u':sports_medal:': u'\U0001F3C5', u':spouting_whale:': u'\U0001F433', u':squared_cl:': u'\U0001F191', u':squared_cool:': u'\U0001F192', u':squared_free:': u'\U0001F193', u':squared_id:': u'\U0001F194', u':squared_katakana_koko:': u'\U0001F201', u':squared_katakana_sa:': u'\U0001F202', u':squared_new:': u'\U0001F195', u':squared_ng:': u'\U0001F196', u':squared_ok:': u'\U0001F197', u':squared_sos:': u'\U0001F198', u':squared_up_with_exclamation_mark:': u'\U0001F199', u':squared_vs:': u'\U0001F19A', u':stadium:': u'\U0001F3DF', u':star2:': u'\U0001F31F', u':star:': u'\U00002B50', u':star_and_crescent:': u'\U0000262A', u':star_of_david:': u'\U00002721', u':stars:': u'\U0001F320', u':station:': u'\U0001F689', u':statue_of_liberty:': u'\U0001F5FD', u':steam_locomotive:': u'\U0001F682', u':steaming_bowl:': u'\U0001F35C', u':stew:': u'\U0001F372', u':stopwatch:': u'\U000023F1', u':straight_ruler:': u'\U0001F4CF', u':strawberry:': u'\U0001F353', u':stuck_out_tongue:': u'\U0001F61B', u':stuck_out_tongue_closed_eyes:': u'\U0001F61D', u':stuck_out_tongue_winking_eye:': u'\U0001F61C', u':studio_microphone:': u'\U0001F399', u':sun_behind_cloud:': u'\U000026C5', u':sun_with_face:': u'\U0001F31E', u':sunflower:': u'\U0001F33B', u':sunglasses:': u'\U0001F60E', u':sunny:': u'\U00002600', u':sunrise:': u'\U0001F305', u':sunrise_over_mountains:': u'\U0001F304', u':sunset_over_buildings:': u'\U0001F307', u':surfer:': u'\U0001F3C4', u':sushi:': u'\U0001F363', u':suspension_railway:': u'\U0001F69F', u':sweat:': u'\U0001F613', u':sweat_drops:': u'\U0001F4A6', u':sweat_smile:': u'\U0001F605', u':sweet_potato:': u'\U0001F360', u':swimmer:': u'\U0001F3CA', u':symbols:': u'\U0001F523', u':synagogue:': u'\U0001F54D', u':syringe:': u'\U0001F489', u':t-shirt:': u'\U0001F455', u':table_tennis_paddle_and_ball:': u'\U0001F3D3', u':taco:': u'\U0001F32E', u':tada:': u'\U0001F389', u':tanabata_tree:': u'\U0001F38B', u':tangerine:': u'\U0001F34A', u':taurus:': u'\U00002649', u':taxi:': u'\U0001F695', u':tea:': u'\U0001F375', u':teacup_without_handle:': u'\U0001F375', u':tear-off_calendar:': u'\U0001F4C6', u':telephone:': u'\U0000260E', u':telephone_receiver:': u'\U0001F4DE', u':telescope:': u'\U0001F52D', u':television:': u'\U0001F4FA', u':ten:': u'\U0001F51F', u':tennis:': u'\U0001F3BE', u':tennis_racquet_and_ball:': u'\U0001F3BE', u':tent:': u'\U000026FA', u':the_horns:': u'\U0001F918', u':thermometer:': u'\U0001F321', u':thinking_face:': u'\U0001F914', u':thought_balloon:': u'\U0001F4AD', u':three_button_mouse:': u'\U0001F5B1', u':thumbs_down_sign:': u'\U0001F44E', u':thumbs_up_sign:': u'\U0001F44D', u':thumbsdown:': u'\U0001F44E', u':thumbsup:': u'\U0001F44D', u':thunder_cloud_and_rain:': u'\U000026C8', u':ticket:': u'\U0001F3AB', u':tiger2:': u'\U0001F405', u':tiger:': u'\U0001F405', u':tiger:': u'\U0001F42F', u':tiger_face:': u'\U0001F42F', u':timer_clock:': u'\U000023F2', u':tired_face:': u'\U0001F62B', u':tm:': u'\U00002122', u':toilet:': u'\U0001F6BD', u':tokyo_tower:': u'\U0001F5FC', u':tomato:': u'\U0001F345', u':tongue:': u'\U0001F445', u':top:': u'\U0001F51D', u':top_hat:': u'\U0001F3A9', u':top_with_upwards_arrow_above:': u'\U0001F51D', u':tophat:': u'\U0001F3A9', u':tornado:': u'\U0001F32A', u':trackball:': u'\U0001F5B2', u':tractor:': u'\U0001F69C', u':trade_mark_sign:': u'\U00002122', u':traffic_light:': u'\U0001F6A5', u':train2:': u'\U0001F686', u':train:': u'\U0001F686', u':train:': u'\U0001F68B', u':tram:': u'\U0001F68A', u':tram_car:': u'\U0001F68B', u':triangular_flag_on_post:': u'\U0001F6A9', u':triangular_ruler:': u'\U0001F4D0', u':trident:': u'\U0001F531', u':trident_emblem:': u'\U0001F531', u':triumph:': u'\U0001F624', u':trolleybus:': u'\U0001F68E', u':trophy:': u'\U0001F3C6', u':tropical_drink:': u'\U0001F379', u':tropical_fish:': u'\U0001F420', u':truck:': u'\U0001F69A', u':trumpet:': u'\U0001F3BA', u':tshirt:': u'\U0001F455', u':tulip:': u'\U0001F337', u':turkey:': u'\U0001F983', u':turtle:': u'\U0001F422', u':tv:': u'\U0001F4FA', u':twisted_rightwards_arrows:': u'\U0001F500', u':two_hearts:': u'\U0001F495', u':two_men_holding_hands:': u'\U0001F46C', u':two_women_holding_hands:': u'\U0001F46D', u':umbrella:': u'\U00002602', u':umbrella_on_ground:': u'\U000026F1', u':umbrella_with_rain_drops:': u'\U00002614', u':unamused:': u'\U0001F612', u':unamused_face:': u'\U0001F612', u':underage:': u'\U0001F51E', u':unicorn_face:': u'\U0001F984', u':unlock:': u'\U0001F513', u':up-pointing_red_triangle:': u'\U0001F53A', u':up-pointing_small_red_triangle:': u'\U0001F53C', u':up:': u'\U0001F199', u':up_down_arrow:': u'\U00002195', u':upside-down_face:': u'\U0001F643', u':upside_down_face:': u'\U0001F643', u':upwards_black_arrow:': u'\U00002B06', u':v:': u'\U0000270C', u':vertical_traffic_light:': u'\U0001F6A6', u':vhs:': u'\U0001F4FC', u':vibration_mode:': u'\U0001F4F3', u':victory_hand:': u'\U0000270C', u':video_camera:': u'\U0001F4F9', u':video_game:': u'\U0001F3AE', u':videocassette:': u'\U0001F4FC', u':violin:': u'\U0001F3BB', u':virgo:': u'\U0000264D', u':volcano:': u'\U0001F30B', u':volleyball:': u'\U0001F3D0', u':vs:': u'\U0001F19A', u':walking:': u'\U0001F6B6', u':waning_crescent_moon:': u'\U0001F318', u':waning_crescent_moon_symbol:': u'\U0001F318', u':waning_gibbous_moon:': u'\U0001F316', u':waning_gibbous_moon_symbol:': u'\U0001F316', u':warning:': u'\U000026A0', u':warning_sign:': u'\U000026A0', u':wastebasket:': u'\U0001F5D1', u':watch:': u'\U0000231A', u':water_buffalo:': u'\U0001F403', u':water_closet:': u'\U0001F6BE', u':water_wave:': u'\U0001F30A', u':watermelon:': u'\U0001F349', u':wave:': u'\U0001F44B', u':waving_black_flag:': u'\U0001F3F4', u':waving_hand_sign:': u'\U0001F44B', u':waving_white_flag:': u'\U0001F3F3', u':wavy_dash:': u'\U00003030', u':waxing_crescent_moon:': u'\U0001F312', u':waxing_crescent_moon_symbol:': u'\U0001F312', u':waxing_gibbous_moon:': u'\U0001F314', u':waxing_gibbous_moon_symbol:': u'\U0001F314', u':wc:': u'\U0001F6BE', u':weary:': u'\U0001F629', u':weary_cat_face:': u'\U0001F640', u':weary_face:': u'\U0001F629', u':wedding:': u'\U0001F492', u':weight_lifter:': u'\U0001F3CB', u':whale2:': u'\U0001F40B', u':whale:': u'\U0001F40B', u':whale:': u'\U0001F433', u':wheel_of_dharma:': u'\U00002638', u':wheelchair:': u'\U0000267F', u':wheelchair_symbol:': u'\U0000267F', u':white_check_mark:': u'\U00002705', u':white_circle:': u'\U000026AA', u':white_down_pointing_backhand_index:': u'\U0001F447', u':white_exclamation_mark_ornament:': u'\U00002755', u':white_flower:': u'\U0001F4AE', u':white_frowning_face:': u'\U00002639', u':white_heavy_check_mark:': u'\U00002705', u':white_large_square:': u'\U00002B1C', u':white_left_pointing_backhand_index:': u'\U0001F448', u':white_medium_small_square:': u'\U000025FD', u':white_medium_square:': u'\U000025FB', u':white_medium_star:': u'\U00002B50', u':white_question_mark_ornament:': u'\U00002754', u':white_right_pointing_backhand_index:': u'\U0001F449', u':white_small_square:': u'\U000025AB', u':white_smiling_face:': u'\U0000263A', u':white_square_button:': u'\U0001F533', u':white_sun_behind_cloud:': u'\U0001F325', u':white_sun_behind_cloud_with_rain:': u'\U0001F326', u':white_sun_with_small_cloud:': u'\U0001F324', u':white_up_pointing_backhand_index:': u'\U0001F446', u':white_up_pointing_index:': u'\U0000261D', u':wind_blowing_face:': u'\U0001F32C', u':wind_chime:': u'\U0001F390', u':wine_glass:': u'\U0001F377', u':wink:': u'\U0001F609', u':winking_face:': u'\U0001F609', u':wolf:': u'\U0001F43A', u':wolf_face:': u'\U0001F43A', u':woman:': u'\U0001F469', u':woman_with_bunny_ears:': u'\U0001F46F', u':womans_boots:': u'\U0001F462', u':womans_clothes:': u'\U0001F45A', u':womans_hat:': u'\U0001F452', u':womans_sandal:': u'\U0001F461', u':womens:': u'\U0001F6BA', u':womens_symbol:': u'\U0001F6BA', u':world_map:': u'\U0001F5FA', u':worried:': u'\U0001F61F', u':worried_face:': u'\U0001F61F', u':wrapped_present:': u'\U0001F381', u':wrench:': u'\U0001F527', u':writing_hand:': u'\U0000270D', u':x:': u'\U0000274C', u':yellow_heart:': u'\U0001F49B', u':yen:': u'\U0001F4B4', u':yin_yang:': u'\U0000262F', u':yum:': u'\U0001F60B', u':zap:': u'\U000026A1', u':zipper-mouth_face:': u'\U0001F910', u':zipper_mouth_face:': u'\U0001F910', u':zzz:': u'\U0001F4A4', } ALIAS_RE = re.compile(r':[+-]?[\w-]+:', flags=re.DOTALL) NEEDSPLIT = ('irc_in_PRIVMSG', 'irc_in_NOTICE', 'irc_in_PART', 'irc_in_QUIT', 'irc_in_KNOCK', 'irc_in_AWAY') HOOKS = ( "away", "cnotice", "cprivmsg", "kick", "knock", "notice", "part", "privmsg", "quit", "wallops", ) def convert_aliases_to_emoji(data, modifier, modifier_data, string): # `unmodified` is text not to have replacements done on it unmodified, modifiable = "", string if modifier in NEEDSPLIT: # if " :" exists in a raw IRC string (once tags have been removed) it # will be the start of the final (trailing) parameter # optionally put IRCv3 tags (and space) in to `unmodified` if string[0] == "@": tags, sep, string = string.partition(" ") unmodified += tags+sep # optionally put :source (and space) in to `unmodified` if string[0] == ":": source, sep, string = string.partition(" ") unmodified += source+sep # split at the first instance of " :" # (`trailing` will be empty string if not found) string, trailing_sep, trailing = string.partition(" :") # put COMMAND (and space) in to `unmodified` command, sep, string = string.partition(" ") unmodified += command+sep if not trailing and string: # we've not got a :trailing param; let's use the last arg instead string, sep, modifiable = string.rpartition(" ") # put all other args (and space) in to `unmodified` unmodified += string+sep else: # we've got a :trailing param. # put all the other args (and " :") in to `unmodified` unmodified += string+trailing_sep modifiable = trailing for alias in ALIAS_RE.findall(modifiable): if alias in EMOJI_ALIASES: modifiable = modifiable.replace(alias, '{} '.format(EMOJI_ALIASES[alias])) return unmodified+modifiable for hook in HOOKS: weechat.hook_modifier( "irc_in_{0}".format(hook), "convert_aliases_to_emoji", "") weechat.hook_modifier("input_text_for_buffer", "convert_aliases_to_emoji", "") weechat-scripts/python/cmus.py0000644000175100017510000001235715112622403015515 0ustar manumanu# Copyright (C) 2013 - Isaac Ross # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see # Made as a port of cmus_xchat-v2.0, also made by Isaac Ross. Due to the nature of weechat's plugin/scripting API, # this was mostly made using find/replace in a text editor import subprocess import weechat import os weechat.register("cmus", "Isaac Ross", "1.1", "GPL2", "Adds ability to control cmus and post the currently playing song in a channel", "", "") def help(): weechat.prnt('', ' --Option-- --Command--') weechat.prnt('', '-Play: /cmus play') weechat.prnt('', '-Pause: /cmus pause') weechat.prnt('', '-Stop: /cmus stop') weechat.prnt('', '-Next Track: /cmus next') weechat.prnt('', '-Previous Track: /cmus prev') weechat.prnt('', '-Toggle Shuffle: /cmus shuffle') weechat.prnt('', '-Player Status: /cmus status') weechat.prnt('', '-Now Playing: /cmusnp') weechat.prnt('', '-NP (filename): /cmus file') weechat.prnt('', ' ') weechat.prnt('', "-NOTE: If the currently playing file lacks artist, album, and title tags, use '/cmus file' to post the filename instead.") weechat.prnt('', '----------------------------------------------------------------------------------------') weechat.prnt('', "If you encounter any problems, feel free to email me at: ") weechat.prnt('', "Keep in mind that most problems will probably be related to cmus-remote, not this script") def np(): cmus = subprocess.getoutput('cmus-remote -Q') lines = cmus.split('\n') #some redundant loops later, but streamline as needed #Only store lines marked as tags tags = [line.split(' ')[1:] for line in lines if line[:3] == 'tag'] title, artist, album = '', '', '' for tag in tags: if tag[0] == 'artist': artist = ' '.join(tag[1:]) elif tag[0] == 'title': title = ' '.join(tag[1:]) elif tag[0] == 'album': album = ' '.join(tag[1:]) else: #Maybe eventually extend the functionality to print other tags? continue nowplaying = "%(artist)s - %(title)s (album: %(album)s)" % \ {"artist": artist, \ "title": title, \ "album": album} return nowplaying def control(data, buffer, args): args = args.split(' ') if args[0].lower() == 'play': os.system('cmus-remote -p') weechat.prnt('', 'Playing...') elif args[0].lower() == 'pause': os.system('cmus-remote -u') weechat.prnt('', 'Paused.') elif args[0].lower() == 'stop': os.system('cmus-remote -s') weechat.prnt('', 'Stopped.') elif args[0].lower() == 'next': os.system('cmus-remote -n') weechat.prnt('', 'Playing next track...') elif args[0].lower() == 'prev': os.system('cmus-remote -r') weechat.prnt('', 'Playing previous track...') elif args[0].lower() == 'shuffle': os.system('cmus-remote -S') weechat.prnt('', 'Toggled shuffle on/off.') elif args[0].lower() == 'status': status = subprocess.getoutput('cmus-remote -Q') status = status.split('\n') for line in status: weechat.prnt('', " -- " + line) elif args[0].lower() == 'help': help() elif args[0].lower() == 'file': filename = subprocess.getoutput('cmus-remote -Q') filename = filename.split('\n') newname = filename[1] newname = newname.replace('file', '', 1) newname = newname.replace(' ', '', 1) newname = newname.rpartition('/') newname = newname[-1] weechat.command('', '/me is currently listening to: ' + newname) else: weechat.prnt('', 'Not a valid option.') help() return weechat.WEECHAT_RC_OK def weechat_np(data, buffer, args): weechat.command(buffer, '/me is currently listening to: ' + np()) return weechat.WEECHAT_RC_OK weechat.hook_command("cmusnp", "Get/send now playing info.", "", "", "", "weechat_np", "") weechat.hook_command("cmus", "Control cmus.", "[file] | [next] | [pause] | [play] | [prev] | [shuffle] | [status] | [stop] | [help]", """ file: Get/send name of the currently playing file. next: Play next file. pause: Pause playback. play: Resume playback. prev: Play previous song. shuffle: Enable shuffle. status: Show status of cmus (same as "cmus-remote -Q" in your shell) stop: Stop playback. help: Alternative (original) help list. Use /cmusnp if you're looking for the now-playing functionality of the script. """ , "", "control", "") weechat-scripts/python/maze.py0000644000175100017510000003536115112622410015500 0ustar manumanu# # Copyright (C) 2012-2022 Sébastien Helleu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Interactive maze generator and solver for WeeChat. # # History: # # 2022-02-20, Sébastien Helleu : # version 1.0.0: first public version # 2012-10-10, Sébastien Helleu : # version 0.0.1: initial release # """Maze generator and solver for WeeChat.""" from dataclasses import dataclass, field from typing import ClassVar, Dict, List, Optional, Tuple import random try: import weechat IMPORT_OK = True except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org/") IMPORT_OK = False SCRIPT_NAME = "maze" SCRIPT_AUTHOR = "Sébastien Helleu " SCRIPT_VERSION = "1.0.0" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Interactive maze generator and solver" SCRIPT_COMMAND = "maze" MAZE_KEYS: Dict[str, Tuple[str, str]] = { "n": ("", "new maze"), "d": ("default", "default size"), "+": ("+", "bigger"), "-": ("-", "smaller"), "s": ("solve", "solve"), "i": ("isolve", "solve interactively"), "r": ("reset", "reset solution"), } @dataclass class Maze: """Maze.""" width: int = 0 height: int = 0 cells: List[int] = field(default_factory=list, init=False) solution: List[tuple[int, int, bool]] = field(default_factory=list, init=False) buffer: str = field(init=False) timer: str = field(default="", init=False) # cell status VISITED: ClassVar[int] = 1 # visited cell TOP: ClassVar[int] = 2 # door opened on top BOTTOM: ClassVar[int] = 4 # door opened on bottom LEFT: ClassVar[int] = 8 # door opened on the left RIGHT: ClassVar[int] = 16 # door opened on the right SOLUTION: ClassVar[int] = 32 # cell part of the solution SOLUTION_INT: ClassVar[int] = 64 # cell displayed for solution def __post_init__(self) -> None: """Initialize maze with the given size.""" self.cells = [0] * (self.width * self.height) self.cells[0] |= self.LEFT self.cells[-1] |= self.RIGHT self.solution = [] self.buffer = Maze.open_buffer() if self.buffer: keys = ", ".join([ f"alt+\"{key}\": {value[1]}" for key, value in MAZE_KEYS.items() ]) weechat.buffer_set( self.buffer, "title", f"Maze {self.width}x{self.height} | {keys}", ) @staticmethod def open_buffer() -> str: """Open maze buffer.""" buf: str = weechat.buffer_search("python", "maze") if not buf: buf = weechat.buffer_new( "maze", "maze_input_buffer", "", "maze_close_buffer", "", ) if buf: weechat.buffer_set(buf, "type", "free") for key, value in MAZE_KEYS.items(): weechat.buffer_set( buf, f"key_bind_meta-{key}", f"/{SCRIPT_COMMAND} {value[0]}".strip(), ) weechat.buffer_set(buf, "display", "1") return buf def get_adjacent_cells( self, col: int, line: int ) -> List[Tuple[int, int, int, int]]: """Get adjacent cells.""" list_cells = [] if col < self.width - 1: # right list_cells.append((col + 1, line, self.RIGHT, self.LEFT)) if line < self.height - 1: # bottom list_cells.append((col, line + 1, self.BOTTOM, self.TOP)) if line > 0: # top list_cells.append((col, line - 1, self.TOP, self.BOTTOM)) if col > 0: # left list_cells.append((col - 1, line, self.LEFT, self.RIGHT)) return list_cells def generate(self) -> None: """Generate a new random maze.""" if not self.cells: return col: int = 0 line: int = 0 self.cells[0] |= self.VISITED stack: List[Tuple[int, int]] = [] while True: pos: int = (line * self.width) + col list_cells: List[Tuple[int, int, int, int]] = ( self.get_adjacent_cells(col, line) ) random.shuffle(list_cells) for col2, line2, to_neighbor, from_neighbor in list_cells: pos2 = (line2 * self.width) + col2 # neighbor not visited? if not self.cells[pos2] & self.VISITED: # open door from (x, y) to neighbor self.cells[pos] |= to_neighbor # open door from neighbor to (x, y) (reverse) self.cells[pos2] |= from_neighbor | self.VISITED stack.append((col, line)) col, line = col2, line2 break else: col, line = stack.pop() if not stack: break def solve(self, interactive: bool = False) -> None: """Solve a maze: find path from entry (0,0) to exit (n,n).""" self.remove_timer() for index, cell in enumerate(self.cells): self.cells[index] = cell & ~(self.VISITED | self.SOLUTION | self.SOLUTION_INT) col: int = 0 line: int = 0 self.cells[0] |= self.SOLUTION self.solution = [(col, line, True)] stack: List[Tuple[int, int]] = [] visited_solution = self.VISITED | self.SOLUTION while not self.cells[-1] & self.SOLUTION: pos: int = (line * self.width) + col self.cells[pos] |= self.VISITED list_cells: List[Tuple[int, int, int, int]] = ( self.get_adjacent_cells(col, line) ) for col2, line2, to_neighbor, _ in list_cells: pos2: int = (line2 * self.width) + col2 # door opened and neighbor not visited/solution? if self.cells[pos] & to_neighbor \ and not self.cells[pos2] & visited_solution: self.cells[pos2] |= visited_solution self.solution.append((col2, line2, True)) stack.append((col, line)) col, line = col2, line2 break else: if not self.cells[-1] & self.SOLUTION: self.cells[pos] &= ~(self.SOLUTION) self.solution.append((col, line, False)) col, line = stack.pop() if not stack: break self.display() if interactive: self.show_interactive_solution() def reset(self) -> None: """Remove solution.""" self.remove_timer() for index, cell in enumerate(self.cells): self.cells[index] = cell & ~(self.SOLUTION | self.SOLUTION_INT) self.display() def display_line(self, line: int) -> None: """Display a line of maze.""" str_line: str if line >= self.height: str_line = ( weechat.color("white") + ("▀" * ((self.width * 2) + 1)) ) else: both_sol: int = self.SOLUTION | self.SOLUTION_INT cell: int = self.cells[line * self.width] color: str = ( "lightred" if cell & self.SOLUTION_INT else "blue" if cell & self.SOLUTION else "black" ) str_line = ( weechat.color(f"{color},white") + ("▄" if cell & self.LEFT else " ") ) for col in range(self.width): cell = self.cells[(line * self.width) + col] color = ( "lightred" if cell & self.SOLUTION_INT else "blue" if cell & self.SOLUTION else "black" ) # door opened on top? if cell & self.TOP: pos: int = ((line - 1) * self.width) + col if line > 0 and self.cells[pos] & both_sol: str_line += weechat.color(f"white,{color}") + " " else: str_line += weechat.color(f"{color},black") + "▄" else: str_line += weechat.color(f"{color},white") + "▄" # door opened on the right? str_line += ( weechat.color(f"{color},white") + ("▄" if cell & self.RIGHT else " ") ) str_line += weechat.color("reset") weechat.prnt_y(self.buffer, line, str_line) def display(self) -> None: """Display maze.""" if not self.buffer: return weechat.buffer_clear(self.buffer) for line in range(self.height + 1): self.display_line(line) def remove_timer(self) -> None: """Reset the timer used to show interactive solution.""" if self.timer: weechat.unhook(self.timer) self.timer = "" def show_interactive_solution(self) -> None: """Show solution.""" self.remove_timer() self.timer = weechat.hook_timer(1, 0, 0, "maze_timer_cb", "") def show_next_solution(self) -> None: """Show next solution step.""" col: int line: int show: bool col, line, show = self.solution.pop(0) pos: int = (line * self.width) + col if show: self.cells[pos] |= self.SOLUTION_INT else: self.cells[pos] &= ~(self.SOLUTION_INT) self.display_line(line) if not self.solution: self.remove_timer() def __del__(self) -> None: """Destructor.""" self.remove_timer() maze: Optional[Maze] = None def maze_input_buffer(data: str, buffer: str, str_input: str) -> int: """Input data in maze buffer.""" # pylint: disable=unused-argument if str_input.lower() == "q": weechat.buffer_close(buffer) else: weechat.command("", f"/{SCRIPT_COMMAND} {str_input}") return weechat.WEECHAT_RC_OK def maze_close_buffer(data: str, buffer: str) -> int: """Called when maze buffer is closed.""" # pylint: disable=unused-argument global maze maze = None return weechat.WEECHAT_RC_OK def maze_timer_cb(data: str, remaining_calls: int) -> int: """Timer used to show solution, one cell by one cell.""" global maze if maze: maze.show_next_solution() return weechat.WEECHAT_RC_OK def maze_get_size(args: str = "") -> Tuple[int, int]: """Get maze size with args, defaulting to current maze or window size.""" global maze width: int = 0 height: int = 0 if args in ("d", "default"): args = "" elif maze: width, height = maze.width, maze.height if args: # size given by user try: items = args.split() if len(items) > 1: width, height = int(items[0]), int(items[1]) else: width, height = int(args), int(args) width = max(width, 2) height = max(height, 2) except ValueError: width, height = 0, 0 if not width or not height: # automatic size with size of window win_width: int = weechat.window_get_integer(weechat.current_window(), "win_chat_width") - 1 win_height: int = weechat.window_get_integer(weechat.current_window(), "win_chat_height") - 1 size: int = min(win_width, win_height) width, height = size, size return width, height def maze_get_other_size(pct_diff: int) -> Tuple[int, int]: """Get another size using a percent of size to add or subtract.""" factor: int = pct_diff // abs(pct_diff) width: int height: int width, height = maze_get_size() add_width: int = max(2, (width * abs(pct_diff)) // 100) add_height: int = max(2, (height * abs(pct_diff)) // 100) width += factor * add_width height += factor * add_height return max(width, 2), max(height, 2) def maze_new(width: int, height: int) -> None: """Create a new maze.""" global maze maze = Maze(width=width, height=height) maze.generate() maze.display() def maze_cmd_cb(data: str, buffer: str, args: str) -> int: """The /maze command.""" global maze width: int height: int if args in ("s", "solve"): if maze: maze.solve() elif args in ("i", "isolve"): if maze: maze.solve(interactive=True) elif args in ("r", "reset"): if maze: maze.reset() else: if args == "+": width, height = maze_get_other_size(30) elif args == "-": width, height = maze_get_other_size(-30) elif args in ("d", "default"): width, height = maze_get_size(args) elif not args or args in ("n", "new"): width, height = maze_get_size() else: error = weechat.prefix("error") weechat.prnt("", f"{error}maze error: unknown option \"{args}\"") return weechat.WEECHAT_RC_OK maze_new(width, height) return weechat.WEECHAT_RC_OK if __name__ == "__main__" and IMPORT_OK: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): weechat.hook_command( SCRIPT_COMMAND, "Generate and solve a maze.", "size || n|new || d|default || s|solve || i|isolve || r|reset " "|| +|-", """\ size: one size (square) or width and height separated by spaces new: regenerate another maze default: regenerate another maze with default size solve: solve the maze and show solution (in blue) isolve: solve then show interactively how solution was found (in red) reset: hide solution +: show a bigger maze -: show a smaller maze All options shown above can be given in input of maze buffer. """, "", "maze_cmd_cb", "", ) weechat-scripts/python/zncplayback.py0000644000175100017510000001477015112622406017053 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2016 Jasper v. Blanckenburg # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Play back all messages the ZNC bouncer received since you last received a # message locally # (this requires the Playback module to be loaded on ZNC) # # Add znc.in/playback either to irc.server_default.capabilites or # to the servers you want to use the Playback module on, then set # plugins.var.python.zncplayback.servers # # History: # # 2022-02-02, Andreas Hackl : # v0.2.2: handle invalid json in SCRIPT_SAVEFILE # 2021-05-06, Sébastien Helleu : # v0.2.1: add compatibility with WeeChat >= 3.2 (XDG directories) # 2019-07-09, Alyssa Ross : # v0.2.0: Make script compatible with Python 3 # 2016-08-27, Jasper v. Blanckenburg : # v0.1.0: Initial release from __future__ import print_function SCRIPT_NAME = "zncplayback" SCRIPT_AUTHOR = "jazzpi" SCRIPT_VERSION = "0.2.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESCRIPTION = "Add support for the ZNC Playback module" SCRIPT_SAVEFILE = "zncplayback_times.json" import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False try: import json import os.path as path from time import time except ImportError as message: print("Missing package(s) for {}: {}".format(SCRIPT_NAME, message)) import_ok = False # Script options zncplayback_settings_default = { "servers": ( "", "Comma-separated list of servers that playback should be fetched for") } zncplayback_settings = {} zncplayback_hooks = { "config": None, "connected": None, "messages": {} } zncplayback_last_times = {} def write_last_times(): """Write the last message times of all servers to disk.""" with open(SCRIPT_SAVEFILE, "w") as fh: json.dump(zncplayback_last_times, fh) def read_last_times(): """Read the last message times of all servers from disk.""" global zncplayback_last_times try: with open(SCRIPT_SAVEFILE, "r") as fh: zncplayback_last_times = json.load(fh) except (json.decoder.JSONDecodeError,FileNotFoundError): for server in zncplayback_settings["servers"].split(","): zncplayback_last_times[server] = 0 def zncplayback_config_cb(data, option, value): """Update in-memory settings when a script option is changed.""" global zncplayback_settings pos = option.rfind(".") if pos > 0: name = option[pos+1:] if name in zncplayback_settings: if name == "servers": old_servers = zncplayback_settings["servers"].split(",") new_servers = value.split(",") # Unhook signals for old servers removed_servers = \ [s for s in old_servers if s not in new_servers] for serv in removed_servers: weechat.unhook(zncplayback_hooks["messages"][serv]) del zncplayback_last_times[serv] # Hook signals for new servers added_servers = \ [s for s in new_servers if s not in old_servers] for serv in added_servers: zncplayback_hooks["messages"][serv] = weechat.hook_signal( "{},irc_raw_in_PRIVMSG".format(serv), "zncplayback_message_cb", serv) zncplayback_last_times[serv] = 0 zncplayback_settings[name] = value return weechat.WEECHAT_RC_OK def zncplayback_connected_cb(data, signal, server): """Fetch playback after connecting to a server.""" if server not in zncplayback_settings["servers"].split(","): return weechat.WEECHAT_RC_OK buf = weechat.buffer_search("irc", "server.{}".format(server)) weechat.command(buf, "/msg *playback PLAY * {}".format(zncplayback_last_times[server])) return weechat.WEECHAT_RC_OK def zncplayback_message_cb(server, signal, message): """Update last time for a server when a PRIVMSG is sent.""" global zncplayback_last_times if server not in zncplayback_settings["servers"].split(","): return weechat.WEECHAT_RC_OK zncplayback_last_times[server] = int(time()) write_last_times() return weechat.WEECHAT_RC_OK if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESCRIPTION, "", ""): options = { 'directory': 'data', } SCRIPT_SAVEFILE = weechat.string_eval_path_home( '%%h/%s' % SCRIPT_SAVEFILE, {}, {}, options, ) # Set default settings version = weechat.info_get("version_number", "") or 0 for option, value in list(zncplayback_settings_default.items()): if weechat.config_is_set_plugin(option): zncplayback_settings[option] = weechat.config_get_plugin( option) else: weechat.config_set_plugin(option, value[0]) zncplayback_settings[option] = value[0] if int(version) >= 0x00030500: weechat.config_set_desc_plugin( option, "%s (default: \"%s\")" % (value[1], value[0])) read_last_times() zncplayback_hooks["config"] = weechat.hook_config( "plugins.var.python.%s.*" % SCRIPT_NAME, "zncplayback_config_cb", "") zncplayback_hooks["connected"] = weechat.hook_signal( "irc_server_connected", "zncplayback_connected_cb", "") for serv in zncplayback_settings["servers"].split(","): zncplayback_hooks["messages"][serv] = weechat.hook_signal( "{},irc_raw_in_PRIVMSG".format(serv), "zncplayback_message_cb", serv) # TODO: Unhook when unloading script weechat-scripts/python/force_nick.py0000644000175100017510000001322715112622406016650 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2015-2018 by Simmo Saan # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # History: # # 2018-06-19, Simmo Saan # version 0.5: ignore channel modes arguments # 2015-08-07, Simmo Saan # version 0.4: option for invite-only channels # 2015-08-07, Simmo Saan # version 0.3: options to control risky channel cycling # 2015-07-03, Simmo Saan # version 0.2: ability to rejoin passworded channels # 2015-07-01, Simmo Saan # version 0.1: initial script # """ Force nick change on channels which disallow it """ from __future__ import print_function SCRIPT_NAME = "force_nick" SCRIPT_AUTHOR = "Simmo Saan " SCRIPT_VERSION = "0.5" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Force nick change on channels which disallow it" IMPORT_OK = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") IMPORT_OK = False SETTINGS = { "cycle_detach": ( "off", "automatically cycle channels which are not open in WeeChat"), "cycle_key": ( "on", "automatically cycle passworded channels (+k)"), "cycle_invite": ( "off", "automatically cycle invite-only channels (+i)") } import re servers = {} def parse_message(signal_data): hashtable = weechat.info_get_hashtable("irc_message_parse", {"message": signal_data}) # parse arguments string into usable pieces args = hashtable["arguments"].split(":", 1) hashtable["args"] = args[0].split() if len(args) > 1: hashtable["text"] = args[1] return hashtable def channel_block(server, channel): fail = None config_cycle = lambda opt: weechat.config_string_to_boolean(weechat.config_get_plugin("cycle_%s" % opt)) channels = weechat.infolist_get("irc_channel", "", "%s,%s" % (server, channel)) if weechat.infolist_next(channels): modes = weechat.infolist_string(channels, "modes") if " " in modes: modes, modes_args = modes.split(" ", 1) if not config_cycle("key") and weechat.infolist_string(channels, "key") != "": fail = "cycle_key" elif not config_cycle("invite") and "i" in modes: fail = "cycle_invite" elif not config_cycle("detach"): fail = "cycle_detach" weechat.infolist_free(channels) if fail: weechat.prnt("", "%s: won't automatically cycle %s.%s: %s" % (SCRIPT_NAME, server, channel, fail)) else: servers[server]["channels"].append(channel) buffer = weechat.buffer_search("irc", server) weechat.command(buffer, "/part %s" % channel) weechat.command(buffer, "/nick %s" % servers[server]["nick"]) def nick_out_cb(data, signal, signal_data): server = signal.split(",")[0] parsed = parse_message(signal_data) nick = parsed["args"][0] if server not in servers: # initialize new nickchange servers[server] = {} servers[server]["channels"] = [] servers[server]["nick"] = nick return weechat.WEECHAT_RC_OK def nick_in_cb(data, signal, signal_data): server = signal.split(",")[0] parsed = parse_message(signal_data) mynick = weechat.info_get("irc_nick", server) if parsed["nick"] == mynick: # nick change worked channels = weechat.infolist_get("irc_channel", "", server) keys = {} while weechat.infolist_next(channels): keys[weechat.infolist_string(channels, "name")] = weechat.infolist_string(channels, "key") buffer = weechat.buffer_search("irc", server) for channel in servers[server]["channels"]: weechat.command(buffer, "/join -noswitch %s %s" % (channel, keys.get(channel, ""))) weechat.infolist_free(channels) servers.pop(server) return weechat.WEECHAT_RC_OK def unreal_cb(data, signal, signal_data): server = signal.split(",")[0] parsed = parse_message(signal_data) match = re.match(r"Can not change nickname while on (#\w+) \(\+N\)", parsed["text"]) if match: channel = match.group(1) channel_block(server, channel) return weechat.WEECHAT_RC_OK def freenode_cb(data, signal, signal_data): server = signal.split(",")[0] parsed = parse_message(signal_data) channel_block(server, parsed["args"][2]) return weechat.WEECHAT_RC_OK if __name__ == "__main__" and IMPORT_OK: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): weechat.hook_signal("*,irc_out1_nick", "nick_out_cb", "") weechat.hook_signal("*,irc_in_nick", "nick_in_cb", "") weechat.hook_signal("*,irc_in_447", "unreal_cb", "") weechat.hook_signal("*,irc_in_435", "freenode_cb", "") for option, value in SETTINGS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) weechat.config_set_desc_plugin(option, "%s (default: \"%s\")" % (value[1], value[0])) weechat-scripts/python/emojis.py0000644000175100017510000002414315112622407016034 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2017 jmui # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # # A simple script to print a random emoji chosen from a list # # Usage: /emojis # # # History: # 2017-11-17 # 0.1 First version # # SCRIPT_NAME = "emojis" SCRIPT_AUTHOR = "jmui " SCRIPT_VERSION = "0.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Send a random emoji to the current buffer" import_ok = True try: import weechat except ImportError: print("Script must be used in WeeChat.") import_ok = False from random import choice emojiList = [ '¢‿¢', '©¿© o', 'ª{•̃̾_•̃̾}ª', '¬_¬', '¯\(º_o)/¯', '¯\(º o)/¯', '¯\_(⊙︿⊙)_/¯', '¯\_(ツ)_/¯', '°ω°', '°Д°', '°‿‿°', '´ ▽ ` )ノ', '¿ⓧ_ⓧﮌ', 'Ò,ó', 'ó‿ó', 'ô⌐ô', 'ôヮô', 'ŎםŎ', 'ŏﺡó', 'ʕ•̫͡•ʔ', 'ʕ•ᴥ•ʔ', 'ʘ‿ʘ', '˚•_•˚', '˚⌇˚', '˚▱˚', 'Σ ◕ ◡ ◕', 'Σ (゚Д゚;)', 'Σ(゚Д゚;≡;゚д゚)', 'Σ(゚Д゚ )', 'Σ(||゚Д゚)', 'Φ,Φ', 'δﺡό', 'σ_σ', 'д_д', 'ф_ф', 'щ(゚Д゚щ)', 'щ(ಠ益ಠщ)', 'щ(ಥДಥщ)', 'Ծ_Ծ', '٩๏̯͡๏۶', '٩๏̯͡๏)۶', '٩◔̯◔۶', '٩(×̯×)۶', '٩(̾●̮̮̃̾•̃̾)۶', '٩(͡๏̯͡๏)۶', '٩(͡๏̯ ͡๏)۶', '٩(ಥ_ಥ)۶', '٩(•̮̮̃•̃)۶', '٩(●̮̮̃•̃)۶', '٩(●̮̮̃●̃)۶', '٩(。͡•‿•。)۶', '٩(-̮̮̃•̃)۶', '٩(-̮̮̃-̃)۶', '۞_۞', '۞_۟۞', '۹ↁﮌↁ', '۹⌤_⌤۹', '॓_॔', '१✌◡✌५', '१|˚–˚|५', 'ਉ_ਉ', 'ଘ_ଘ', 'இ_இ', 'ఠ_ఠ', 'రృర', 'ಠ¿ಠi', 'ಠ‿ಠ', 'ಠ⌣ಠ', 'ಠ╭╮ಠ', 'ಠ▃ಠ', 'ಠ◡ಠ', 'ಠ益ಠ', 'ಠ益ಠ', 'ಠ︵ಠ凸', 'ಠ , ಥ', 'ಠ.ಠ', 'ಠoಠ', 'ಠ_ృ', 'ಠ_ಠ', 'ಠ_๏', 'ಠ~ಠ', 'ಡ_ಡ', 'ತಎತ', 'ತ_ತ', 'ಥдಥ', 'ಥ‿ಥ', 'ಥ⌣ಥ', 'ಥ◡ಥ', 'ಥ﹏ಥ', 'ಥ_ಥ', 'ಭ_ಭ', 'ರ_ರ', 'ಸ , ໖', 'ಸ_ಸ', 'ക_ക', 'อ้_อ้', 'อ_อ', 'โ๏௰๏ใ ื', '๏̯͡๏﴿', '๏̯͡๏', '๏̯͡๏﴿', '๏[-ิิ_•ิ]๏', '๏_๏', '໖_໖', '༺‿༻', 'ლ(´ڡ`ლ)', 'ლ(́◉◞౪◟◉‵ლ)', 'ლ(ಠ益ಠლ)', 'ლ(╹◡╹ლ)', 'ლ(◉◞౪◟◉‵ლ)', 'ლ,ᔑ•ﺪ͟͠•ᔐ.ლ', 'ᄽὁȍ ̪ őὀᄿ', 'ᕕ( ᐛ )ᕗ', 'ᕙ(⇀‸↼‶)ᕗ', 'ᕦ(ò_óˇ)ᕤ', 'ᶘ ᵒᴥᵒᶅ', '‘︿’', '•▱•', '•✞_✞•', '•(⌚_⌚)•', '•_•)', '‷̗ↂ凸ↂ‴̖', '‹•.•›', '‹› ‹(•¿•)› ‹›', '‹(ᵒᴥᵒ­­­­­)›', '‹(•¿•)›', 'ↁ_ↁ', '⇎_⇎', '∩(︶▽︶)∩', '∩( ・ω・)∩', '≖‿≖', '≧ヮ≦', '⊂•⊃_⊂•⊃', '⊂⌒~⊃。Д。)⊃', '⊂(◉‿◉)つ', '⊂(゚Д゚,,⊂⌒`つ', '⊙ω⊙', '⊙▂⊙', '⊙▃⊙', '⊙△⊙', '⊙︿⊙', '⊙﹏⊙', '⊙0⊙', '⊛ठ̯⊛', '⋋ō_ō`', '━━━ヽ(ヽ(゚ヽ(゚∀ヽ(゚∀゚ヽ(゚∀゚)ノ゚∀゚)ノ∀゚)ノ゚)ノ)ノ━━━', '┌∩┐(◕_◕)┌∩┐', '┌( ಠ_ಠ)┘', '┌( ಥ_ಥ)┘', '╚(•⌂•)╝', '╭╮╭╮☜{•̃̾_•̃̾}☞╭╮╭╮', '╭✬⌢✬╮', '╮(─▽─)╭', '╯‵Д′)╯彡┻━┻', '╰☆╮', '□_□', '►_◄', '◃┆◉◡◉┆▷', '◉△◉', '◉︵◉', '◉_◉', '○_○', '●¿●\ ~', '●_●', '◔̯◔', '◔ᴗ◔', '◔ ⌣ ◔', '◔_◔', '◕ω◕', '◕‿◕', '◕◡◕', '◕ ◡ ◕', '◖♪_♪|◗', '◖|◔◡◉|◗', '◘_◘', '◙‿◙', '◜㍕◝', '◪_◪', '◮_◮', '☁ ☝ˆ~ˆ☂', '☆¸☆', '☉‿⊙', '☉_☉', '☐_☐', '☜(⌒▽⌒)☞', '☜(゚ヮ゚☜)', '☜-(ΘLΘ)-☞', '☝☞✌', '☮▁▂▃▄☾ ♛ ◡ ♛ ☽▄▃▂▁☮', '☹_☹', '☻_☻', '☼.☼', '☾˙❀‿❀˙☽', '♥‿♥', '♥╣[-_-]╠♥', '♥╭╮♥', '♥◡♥', '✌♫♪˙❤‿❤˙♫♪✌', '✌.ʕʘ‿ʘʔ.✌', '✌.|•͡˘‿•͡˘|.✌', '✖‿✖', '✖_✖', '❐‿❑', '⨀_⨀', '⨀_Ꙩ', '⨂_⨂', '〆(・∀・@)', '《〠_〠》', '【•】_【•】', '〠_〠', '〴⋋_⋌〵', 'の� �の', 'ニガー? ━━━━━━(゚∀゚)━━━━━━ ニガー?', 'ヽ(´ー` )ノ', 'ヽ(๏∀๏ )ノ', 'ヽ(`Д´)ノ', 'ヽ(o`皿′o)ノ', 'ヽ(`Д´)ノ', 'ㅎ_ㅎ', '乂◜◬◝乂', '凸ಠ益ಠ)凸', '句_句', 'Ꙩ⌵Ꙩ', 'Ꙩ_Ꙩ', 'ꙩ_ꙩ', 'Ꙫ_Ꙫ', 'ꙫ_ꙫ', 'ꙮ_ꙮ', '흫_흫', '句_句', '﴾͡๏̯͡๏﴿', '¯\(ºдಠ)/¯', '(·×·)', '(⌒Д⌒)', '(╹ェ╹)', '(♯・∀・)⊃', '( ´∀`)☆', '( ´∀`)', '(゜Д゜)', '(・∀・)', '(・A・)', '(゚∀゚)', '( ̄へ ̄)', '( ´☣///_ゝ///☣`)', '( つ Д `)', '_☆( ´_⊃`)☆_', '。◕‿‿◕。', '。◕ ‿ ◕。', '!⑈ˆ~ˆ!⑈', '!(`・ω・。)', '(¬‿¬)', '(¬▂¬)', '(¬_¬)', '(°ℇ °)', '(°∀°)', '(´ω`)', '(´◉◞౪◟◉)', '(´ヘ`;)', '(´・ω・`)', '(´ー`)', '(ʘ‿ʘ)', '(ʘ_ʘ)', '(˚இ˚)', '(͡๏̯͡๏)', '(ΘεΘ;)', '(ι´Д`)ノ', '(Ծ‸ Ծ)', '(॓_॔)', '(० ्०)', '(ு८ு_ .:)', '(ಠ‾ಠ)', '(ಠ‿ʘ)', '(ಠ‿ಠ)', '(ಠ⌣ಠ)', '(ಠ益ಠ ╬)', '(ಠ益ಠ)', '(ಠ_ృ)', '(ಠ_ಠ)', '(ಥ﹏ಥ)', '(ಥ_ಥ)', '(๏̯͡๏ )', '(ღ˘⌣˘ღ) ♫・*:.。. .。.:*・', '(ღ˘⌣˘ღ)', '(ᵔᴥᵔ)', '(•ω•)', '(•‿•)', '(•⊙ω⊙•)', '(• ε •)', '(∩▂∩)', '(∩︵∩)', '(∪ ◡ ∪)', '(≧ω≦)', '(≧◡≦)', '(≧ロ≦)', '(⊙ヮ⊙)', '(⊙_◎)', '(⋋▂⋌)', '(⌐■_■)', '(─‿‿─)', '(┛◉Д◉)┛┻━┻', '(╥_╥)', '(╬ಠ益ಠ)', '(╬◣д◢)', '(╬ ಠ益ಠ)', '(╯°□°)╯︵ ┻━┻', '(╯ಊ╰)', '(╯◕_◕)╯', '(╯︵╰,)', '(╯3╰)', '(╯_╰)', '(╹◡╹)凸', '(▰˘◡˘▰)', '(●´ω`●)', '(●´∀`●)', '(◑‿◐)', '(◑◡◑)', '(◕‿◕✿)', '(◕‿◕)', '(◕‿-)', '(◕︵◕)', '(◕ ^ ◕)', '(◕_◕)', '(◜௰◝)', '(◡‿◡✿)', '(◣_◢)', '(☞゚∀゚)☞', '(☞゚ヮ゚)☞', '(☞゚ ∀゚ )☞', '(☼◡☼)', '(☼_☼)', '(✌゚∀゚)☞', '(✖╭╮✖)', '(✪㉨✪)', '(✿◠‿◠)', '(✿ ♥‿♥)', '( ・∀・)', '( ・ัω・ั)?', '( ゚∀゚)o彡゜', '(。・_・。)', '(つд`)', '(づ。◕‿‿◕。)づ', '(ノಠ益ಠ)ノ彡┻━┻', '(ノ ◑‿◑)ノ', '(ノ_・。)', '(・∀・ )', '(屮゚Д゚)屮', '(︶ω︶)', '(︶︹︺)', '(;一_一)', '(`・ω・´)”', '(。◕‿‿◕。)', '(。◕‿◕。)', '(。◕ ‿ ◕。)', '(。♥‿♥。)', '(。・ω..・)っ', '(・ェ-)', '(ノ◕ヮ◕)ノ*:・゚✧', '(゚Д゚)', '(゚Д゚)y─┛~~', '(゚∀゚)', '(゚ヮ゚)', '( ̄□ ̄)', '( ̄。 ̄)', '( ̄ー ̄)', '( ̄(エ) ̄)', '( °٢° )', '( ´_ゝ`)', '( ͡° ͜ʖ ͡°)', '( ͡~ ͜ʖ ͡°)', '( ಠ◡ಠ )', '( •_•)>⌐■-■', '(  ゚,_ゝ゚)', '( ・ิз・ิ)', '( ゚д゚)、', '( ^▽^)σ)~O~)', '((((゜д゜;))))', '(*´д`*)', '(*..Д`)', '(*..д`*)', '(*~▽~)', '(-’๏_๏’-)', '(-_- )ノ', '(/◔ ◡ ◔)/', '(///_ಥ)', '(;´Д`)', '(=ω=;)', '(=゜ω゜)', '(>\'o\')> ♥ <(\'o\'<)', '(n˘v˘•)¬', '(o´ω`o)', '(V)(°,,°)(V)', '(\/) (°,,°) (\/)', '(^▽^)', '(`・ω・´)', '(~ ̄▽ ̄)~', '\= (゚д゚)ウ', '@_@', 'd(*⌒▽⌒*)b', 'o(≧∀≦)o', 'o(≧o≦)o', 'q(❂‿❂)p', 'y=ー( ゚д゚)・∵.', '\˚ㄥ˚\ ', '\ᇂ_ᇂ\ ', '\(ಠ ὡ ಠ )/', '\(◕ ◡ ◕\)', '^̮^', '^ㅂ^', '_(͡๏̯͡๏)_', '{´◕ ◡ ◕`}', '\{ಠ_ಠ\}__,,|,', '{◕ ◡ ◕}', ] def print_face(data, buf, args): weechat.command(buf, choice(emojiList)) return weechat.WEECHAT_RC_OK if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, "", "", "", "print_face", "") weechat-scripts/python/xfer_setip.py0000644000175100017510000001465715112622400016720 0ustar manumanu# Copyright (c) 2010-2012 by Stephan Huebner # # Intended use: # # Set ip-setting to the correct external IP whenever one connects to a server # (so that dcc-sending works) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # History: # v 0.1 - first public release # v 0.2 - changed server from which the IP is gathered # - switched from Pythons' urlopen to weechats' url transfer # (therefore 0.3.7 is required) # - some small tweaks in description and (hopefully) and # IP-checking, to make sure that I really got a correct one. # - changed contact Email to one that actually exists :) # v 0.3 - add option to change service from which the IP is gathered # - better recognition of ipv4 addresses and support of ipv6 # - add mute option # v 0.4 - check if xfer plugin is loaded. # v 0.5 - make script python3 compatible. from __future__ import print_function SCR_NAME = "xfer_setip" SCR_AUTHOR = "Stephan Huebner " SCR_VERSION = "0.5" SCR_LICENSE = "GPL3" SCR_DESC = "Set apropriate xfer-option for external ip" SCR_COMMAND = "xfer_setip" import_ok = True ip_from_option = 0 OPTIONS = { 'mute' : ('off','hide output'), 'url' : ('http://checkip.dyndns.org/','url to fetch'), } import re try: from html.parser import HTMLParser # Python 3 except ImportError: from HTMLParser import HTMLParser # Python 2 try: import weechat as w except ImportError: print("Script must be run under weechat. http://www.weechat.org") import_ok = False def alert(myString): w.prnt("", myString) return # create a subclass and override the handler methods class MyHTMLParser(HTMLParser): def handle_data(self, data): global OPTIONS, ip_from_option data=data.strip() ipv6 = re.compile(r""" \s* # Leading whitespace (?!.*::.*::) # Only a single whildcard allowed (?:(?!:)|:(?=:)) # Colon iff it would be part of a wildcard (?: # Repeat 6 times: [0-9a-f]{0,4} # A group of at most four hexadecimal digits (?:(?<=::)|(? # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Flood'it game for WeeChat. # (mouse supported with WeeChat >= 0.3.6) # # History: # # 2012-01-03, Sebastien Helleu : # version 0.4: make script compatible with Python 3.x # 2011-09-29, Sebastien Helleu : # version 0.3: fix error on floodit buffer after /upgrade # 2011-08-20, Sebastien Helleu : # version 0.2: add "q" (or "quit") to close floodit buffer # 2011-08-20, Sebastien Helleu : # version 0.1: initial release # SCRIPT_NAME = 'floodit' SCRIPT_AUTHOR = 'Sebastien Helleu ' SCRIPT_VERSION = '0.4' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = 'Flood\'it game' SCRIPT_COMMAND = 'floodit' import_ok = True try: import weechat except ImportError: print('This script must be run under WeeChat.') print('Get WeeChat now at: http://www.weechat.org/') import_ok = False try: import random, copy except ImportError as message: print('Missing package(s) for %s: %s' % (SCRIPT_NAME, message)) import_ok = False floodit = { 'buffer' : '', 'mode' : 'single', 'board' : [], 'sizes' : { 14: 25, 21: 35, 28: 50 }, 'size' : 14, 'zoom' : 1, 'colors' : [], 'color' : 0, 'count' : 0, 'count_max': 25, 'end' : '', 'timer' : '', } # script options floodit_settings_default = { 'colors' : ['blue,red,green,yellow,magenta,cyan', 'comma-separated list of 6 colors for squares'], 'zoom' : ['', 'zoom for board (0-N, empty means automatic zoom according to size of window)'], } floodit_settings = {} # mouse keys floodit_mouse_keys = { '@chat(python.floodit):button1': '/window ${_window_number};hsignal:floodit_mouse' } def floodit_display(clear=False): """Display status and board.""" global floodit if not floodit['buffer']: return if clear: weechat.buffer_clear(floodit['buffer']) spaces = ' ' * ((floodit['zoom'] + 1) * 2) str_line = '' for index, color in enumerate(floodit['colors']): str_select = [' ', ' '] if floodit['color'] == index: str_select = ['»', '«'] str_line += '%s%s%s%s%s%s%s' % (weechat.color('white,default'), str_select[0], weechat.color(',%s' % color), spaces, weechat.color('white,default'), str_select[1], spaces[0:-2]) str_status = '' str_end = '' if floodit['mode'] == 'single': board = copy.deepcopy(floodit['board']) floodit_flood_xy(board, 0, 0, board[0][0]) percent = (floodit_count_color(board, -1) * 100) // (floodit['size'] * floodit['size']) str_status = '%2d/%d%s (%d%%)' % (floodit['count'], floodit['count_max'], weechat.color('chat'), percent) message_end = { 'win': '** CONGRATS! **', 'lose': '...GAME OVER!...' } elif floodit['mode'] == 'versus': colors = ['yellow', 'lightred'] board = copy.deepcopy(floodit['board']) floodit_flood_xy(board, 0, 0, board[0][0]) count_player = floodit_count_color(board, -1) board = copy.deepcopy(floodit['board']) floodit_flood_xy(board, floodit['size'] - 1, floodit['size'] - 1, board[floodit['size'] - 1][floodit['size'] - 1]) count_computer = floodit_count_color(board, -1) if count_player == count_computer: colors[1] = 'yellow' elif count_computer > count_player: colors.reverse() str_status = '%sYou: %d%s / %sWee: %d' % (weechat.color(colors[0]), count_player, weechat.color('default'), weechat.color(colors[1]), count_computer) message_end = { 'win': '** YOU WIN! **', 'lose': '...You lose...', 'equality': 'Equality!' } str_end = '%s%s' % (weechat.color('white'), message_end.get(floodit['end'], '')) weechat.prnt_y(floodit['buffer'], 0, '%s %s %s' % (str_line, str_status, str_end)) for i in range (0, floodit['zoom']): weechat.prnt_y(floodit['buffer'], 1 + i, str_line) weechat.prnt_y(floodit['buffer'], floodit['zoom'] + 1, '%s%s' % (weechat.color('blue'), '─' * (floodit['size'] * ((floodit['zoom'] + 1) * 2)))) for y, line in enumerate(floodit['board']): str_line = '' for color in line: str_line += '%s%s' % (weechat.color(',%s' % floodit['colors'][color]), spaces) str_line += '%s' % weechat.color('chat') for i in range (0, floodit['zoom'] + 1): weechat.prnt_y(floodit['buffer'], floodit['zoom'] + 2 + (y * (floodit['zoom'] + 1)) + i, str_line) def floodit_adjust_zoom(): """Choose zoom according to size of window.""" global floodit, floodit_settings floodit['zoom'] = -1 if floodit_settings['zoom']: try: floodit['zoom'] = int(floodit_settings['zoom']) except: floodit['zoom'] = -1 if floodit['zoom'] < 0: width = weechat.window_get_integer(weechat.current_window(), 'win_chat_width') height = weechat.window_get_integer(weechat.current_window(), 'win_chat_height') for i in range(10, -1, -1): if width >= floodit['size'] * ((i + 1) * 2) and height >= (floodit['size'] * (i + 1)) + i + 2: floodit['zoom'] = i break if floodit['zoom'] < 0: floodit['zoom'] = 0 def floodit_set_colors(): """Set list of colors using settings.""" global floodit, floodit_settings, floodit_settings_default floodit['colors'] = floodit_settings['colors'].split(',') if len(floodit['colors']) != 6: weechat.prnt('', '%sfloodit: invalid colors (list must have 6 colors)' % weechat.prefix('error')) floodit['colors'] = floodit_settings_default['colors'][0].split(',') def floodit_config_cb(data, option, value): """Called when a script option is changed.""" global floodit_settings pos = option.rfind('.') if pos > 0: name = option[pos+1:] if name in floodit_settings: floodit_settings[name] = value if name == 'colors': floodit_set_colors() elif name == 'zoom': floodit_adjust_zoom() floodit_display() return weechat.WEECHAT_RC_OK def floodit_new_game(): """Create a new game: initialize board and some variables.""" global floodit floodit['board'] = [] for y in range(0, floodit['size']): line = [] for x in range(0, floodit['size']): line.append(random.randint(0, 5)) floodit['board'].append(line) if floodit['mode'] == 'versus': floodit['board'][floodit['size'] - 1][floodit['size'] - 1] = floodit['board'][0][0] floodit['color'] = 0 floodit['count'] = 0 floodit['end'] = '' floodit_display() def floodit_change_size(add): """Change size of board.""" global floodit keys = sorted(floodit['sizes']) index = keys.index(floodit['size']) + add if index >= 0 and index < len(keys): floodit['size'] = keys[index] floodit['count_max'] = floodit['sizes'][floodit['size']] weechat.buffer_clear(floodit['buffer']) floodit_adjust_zoom() floodit_new_game() def floodit_timer_cb(data, remaining_calls): """Timer for demo mode.""" global floodit floodit['color'] = floodit_find_best(0, 0) floodit_user_flood() if floodit['end']: weechat.unhook(floodit['timer']) floodit['timer'] = '' return weechat.WEECHAT_RC_OK def floodit_input_buffer(data, buffer, input): """Input data in floodit buffer.""" global floodit if input: args = input.split(' ') if args[0] in ('d', 'demo'): if not floodit['timer']: delay = 500 if len(args) > 1: try: delay = int(args[1]) except: delay = 500 if delay <= 0: delay = 1 if floodit['end']: floodit_new_game() floodit['timer'] = weechat.hook_timer(delay, 0, 0, 'floodit_timer_cb', '') elif args[0] in ('s', 'single'): floodit['mode'] = 'single' floodit_new_game() elif args[0] in ('v', 'versus'): floodit['mode'] = 'versus' floodit_new_game() elif args[0] in ('n', 'new'): floodit_new_game() elif args[0] in ('q', 'quit'): weechat.buffer_close(floodit['buffer']) elif args[0] == '+': floodit_change_size(+1) elif args[0] == '-': floodit_change_size(-1) return weechat.WEECHAT_RC_OK def floodit_close_buffer(data, buffer): """Called when floodit buffer is closed.""" global floodit if floodit['timer']: weechat.unhook(floodit['timer']) floodit['timer'] = '' floodit['buffer'] = '' return weechat.WEECHAT_RC_OK def floodit_init(): """Init floodit: create buffer, adjust zoom, new game.""" global floodit, floodit_settings if floodit['buffer']: return floodit['buffer'] = weechat.buffer_search('python', 'floodit') if not floodit['buffer']: floodit['buffer'] = weechat.buffer_new('floodit', 'floodit_input_buffer', '', 'floodit_close_buffer', '') if floodit['buffer']: weechat.buffer_set(floodit['buffer'], 'type', 'free') weechat.buffer_set(floodit['buffer'], 'title', 'Flood it! | alt-f or mouse: flood, alt-n: new game, alt-+/-: adjust board zoom | ' 'Command line: (n)ew, (s)ingle, (v)ersus, (d)emo (+delay), +/-: change size, (q)uit') weechat.buffer_set(floodit['buffer'], 'key_bind_meta2-D', '/floodit left') weechat.buffer_set(floodit['buffer'], 'key_bind_meta2-C', '/floodit right') weechat.buffer_set(floodit['buffer'], 'key_bind_meta-f', '/floodit flood') weechat.buffer_set(floodit['buffer'], 'key_bind_meta-n', '/floodit new') weechat.buffer_set(floodit['buffer'], 'key_bind_meta-+', '/floodit zoom') weechat.buffer_set(floodit['buffer'], 'key_bind_meta--', '/floodit dezoom') weechat.buffer_set(floodit['buffer'], 'key_bind_meta-C', '/floodit computer') if floodit['buffer']: floodit_adjust_zoom() floodit_new_game() def floodit_flood_xy(board, x, y, color): """Flood a board at (x,y) with color.""" global floodit board[y][x] = -1 if y > 0 and board[y-1][x] == color: floodit_flood_xy(board, x, y - 1, color) if y < floodit['size'] - 1 and board[y+1][x] == color: floodit_flood_xy(board, x, y + 1, color) if x > 0 and board[y][x-1] == color: floodit_flood_xy(board, x - 1, y, color) if x < floodit['size'] - 1 and board[y][x+1] == color: floodit_flood_xy(board, x + 1, y, color) def floodit_flood_end(board, color): """End of flood: replace the -1 by color.""" for y, line in enumerate(board): for x, c in enumerate(line): if c == -1: board[y][x] = color def floodit_count_color(board, color): """Count number of times a color is used in board.""" global floodit count = 0 for line in board: count += line.count(color) return count def floodit_flood(x, y, color): """Flood board at (x,y) with color, and check if game has ended.""" global floodit floodit_flood_xy(floodit['board'], x, y, floodit['board'][y][x]) floodit_flood_end(floodit['board'], color) floodit['count'] += 1 if floodit['mode'] == 'single': if floodit_count_color(floodit['board'], floodit['board'][0][0]) == floodit['size'] * floodit['size']: floodit['end'] = 'win' elif floodit['count'] == floodit['count_max']: floodit['end'] = 'lose' elif floodit['mode'] == 'versus': board = copy.deepcopy(floodit['board']) floodit_flood_xy(board, 0, 0, board[0][0]) count1 = floodit_count_color(board, -1) board = copy.deepcopy(floodit['board']) floodit_flood_xy(board, floodit['size'] - 1, floodit['size'] - 1, board[floodit['size'] - 1][floodit['size'] - 1]) count2 = floodit_count_color(board, -1) if count1 + count2 == floodit['size'] * floodit['size']: if count1 > count2: floodit['end'] = 'win' elif count1 < count2: floodit['end'] = 'lose' else: floodit['end'] = 'equality' floodit_display() def floodit_build_combs(combs, curlist, maxsize, excludecolor): """Build list of combinations to try for computer AI.""" global floodit if len(curlist) >= maxsize: combs.append(curlist) else: curlist.append(-1) colors = list(range(0, len(floodit['colors']))) random.shuffle(colors) for i in colors: if i == excludecolor: continue if i != curlist[-2]: curlist[-1] = i floodit_build_combs(combs, list(curlist), maxsize, excludecolor) def floodit_compare_scores(scores1, scores2): """Compare two list of scores.""" sum1 = sum(scores1) sum2 = sum(scores2) if sum1 > sum2: return 1 elif sum1 < sum2: return -1 else: if scores1 > scores2: return 1 elif scores1 < scores2: return -1 else: return 0 def floodit_find_best(x, y): """Find best color for (x,y) (computer AI).""" global floodit combs = [] excludecolor = -1 if floodit['mode'] == 'versus': if x == 0: excludecolor = floodit['board'][floodit['size'] - 1][floodit['size'] - 1] else: excludecolor = floodit['board'][0][0] floodit_build_combs(combs, [floodit['board'][y][x]], 3, excludecolor) bestscores = [] bestcolor = 0 for comb in combs: board = copy.deepcopy(floodit['board']) scores = [] for color in comb[1:]: floodit_flood_xy(board, x, y, board[y][x]) floodit_flood_end(board, color) floodit_flood_xy(board, x, y, board[y][x]) scores.append(floodit_count_color(board, -1)) floodit_flood_end(board, color) if floodit_compare_scores(scores, bestscores) > 0: bestscores = scores bestcolor = comb[1] return bestcolor def floodit_user_flood(): """Action flood from user, and then computer plays if mode is 'versus'.""" global floodit if floodit['color'] != floodit['board'][0][0]: if floodit['mode'] != 'versus' or floodit['color'] != floodit['board'][floodit['size'] - 1][floodit['size'] - 1]: floodit_flood(0, 0, floodit['color']) if floodit['mode'] == 'versus' and not floodit['end']: floodit_flood(floodit['size'] - 1, floodit['size'] - 1, floodit_find_best(floodit['size'] - 1, floodit['size'] - 1)) def floodit_cmd_cb(data, buffer, args): """The /floodit command.""" global floodit if args in ('single', 'versus'): floodit['mode'] = args floodit_init() if floodit['buffer']: weechat.buffer_set(floodit['buffer'], 'display', '1') if not floodit['end']: if args == 'left': if floodit['color'] > 0: floodit['color'] -= 1 else: floodit['color'] = len(floodit['colors']) - 1 floodit_display() elif args == 'right': if floodit['color'] < len(floodit['colors']) - 1: floodit['color'] += 1 else: floodit['color'] = 0 floodit_display() elif args == 'flood': floodit_user_flood() elif args == 'computer': floodit['color'] = floodit_find_best(0, 0) floodit_user_flood() if args == 'new': floodit_new_game() elif args == 'zoom': floodit['zoom'] += 1 floodit_display(True) elif args == 'dezoom': if floodit['zoom'] > 0: floodit['zoom'] -= 1 floodit_display(True) return weechat.WEECHAT_RC_OK def floodit_mouse_cb(data, hsignal, hashtable): """Mouse callback.""" global floodit if not floodit['end']: x = int(hashtable.get('_chat_line_x', '-1')) y = int(hashtable.get('_chat_line_y', '-1')) if x >= 0 and y >= 0: color = -1 if y <= floodit['zoom']: multiplier = (floodit['zoom'] + 1) * 4 add = 2 + ((floodit['zoom'] + 1) * 2) for i in range (0, len(floodit['colors'])): if x >= i * multiplier and x < (i * multiplier) + add: color = i break elif y >= floodit['zoom'] + 2: x = x // ((floodit['zoom'] + 1) * 2) y = (y - floodit['zoom'] - 2) // (floodit['zoom'] + 1) if y < floodit['size'] and x < floodit['size']: color = floodit['board'][y][x] if color >= 0: floodit['color'] = color floodit_user_flood() return weechat.WEECHAT_RC_OK if __name__ == '__main__' and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): # set default settings version = weechat.info_get("version_number", "") or 0 for option, value in floodit_settings_default.items(): if weechat.config_is_set_plugin(option): floodit_settings[option] = weechat.config_get_plugin(option) else: weechat.config_set_plugin(option, value[0]) floodit_settings[option] = value[0] if int(version) >= 0x00030500: weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) floodit_set_colors() # mouse support if int(version) >= 0x00030600: weechat.key_bind('mouse', floodit_mouse_keys) weechat.hook_hsignal('floodit_mouse', 'floodit_mouse_cb', '') # detect config changes weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, 'floodit_config_cb', '') # add command weechat.hook_command(SCRIPT_COMMAND, 'Flood''it game.', '[single|versus]', 'single: play in single mode (default)\n' 'versus: play versus computer\n\n' 'Single mode:\n' '- Choose a color for the upper left square, this will paint ' 'this square and all squares next to this one (having same color) ' 'with your color.\n' '- You win if all squares are same color.\n' '- Maximum number of floods is 25, 35 or 50 (according to size).\n\n' 'Versus mode:\n' '- You paint the upper left square, WeeChat paints bottom right.\n' '- You can not paint with last color used by WeeChat.\n' '- Game ends when neither you nor WeeChat can paint new squares any more.\n' '- You win if you have more squares of your color than WeeChat.', 'single|versus', 'floodit_cmd_cb', '') # if buffer already exists (after /upgrade), init floodit if weechat.buffer_search('python', 'floodit'): floodit_init() weechat-scripts/python/responsive_layout.py0000644000175100017510000003021115112622404020326 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2014 - 2019 Stefan Wold # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # (This script requires WeeChat 0.4.3 or higher). # # WeeChat script for responsive layout based on terminal height and width. # # # Source and changes available on GitHUB: https://github.com/Ratler/ratlers-weechat-scripts # # Commands: # /rlayout from __future__ import print_function SCRIPT_NAME = "responsive_layout" SCRIPT_AUTHOR = "Stefan Wold " SCRIPT_VERSION = "0.8" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Responsive layout will automatically apply layouts based on the terminals current size." SCRIPT_COMMAND = "rlayout" SETTINGS = { "default_nicklist": ("on", "Global setting to always show nicklist when layout switches."), "debug": ("off", "Script debug output"), } LAYOUT_LIST = [] import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") import_ok = False try: import re from operator import itemgetter except ImportError as err: print("Missing module(s) for %s: %s" % (SCRIPT_NAME, err)) import_ok = False def _print(message, buf=""): weechat.prnt(buf, "%s: %s" % (SCRIPT_NAME, message)) def _debug(message, buf=""): if weechat.config_get_plugin('debug') == 'on': weechat.prnt(buf, "+++ %s (debug): %s" % (SCRIPT_NAME, message)) def responsive_cb(data, signal, signal_data): term_height = int(weechat.info_get("term_height", "")) term_width = int(weechat.info_get("term_width", "")) try: apply_layout = None for layout, width, height in LAYOUT_LIST: if term_height <= int(height) or term_width <= int(width): apply_layout = layout break if apply_layout is None: # Always apply the last layout if term width/height is larger than configured layouts apply_layout = LAYOUT_LIST[-1][0] if layout_exist(apply_layout) and not layout_current(apply_layout): _print("Applying layout %s" % apply_layout) weechat.command("", "/layout apply %s" % apply_layout) toggle_nick_list(apply_layout) weechat.bar_item_update("rlayout") except ValueError: _print("Height or width is not in number form, ignoring.") return weechat.WEECHAT_RC_OK def layout_current(layout): infolist = weechat.infolist_get("layout", "", "") current = False while weechat.infolist_next(infolist): if weechat.infolist_integer(infolist, "current_layout") == 1 and \ weechat.infolist_string(infolist, "name") == layout: current = True break weechat.infolist_free(infolist) return current def layout_exist(layout): infolist = weechat.infolist_get("layout", "", "") found = False while weechat.infolist_next(infolist): if layout == weechat.infolist_string(infolist, "name"): found = True break weechat.infolist_free(infolist) return found def toggle_nick_list(layout): """ Check configuration whether nick list bar should be on or off for the provided layout. """ value = weechat.config_get_plugin("layout.%s.nicklist" % layout) if value == "": value = weechat.config_get_plugin("default_nicklist") if value == "on": weechat.command("", "/bar show nicklist") elif value == "off": weechat.command("", "/bar hide nicklist") def rlayouts_list(): """ Return a list of configured rlayouts. """ layouts = [] pattern = re.compile(r"^plugins\.var\.python\.%s\.layout\.(.+)\." % SCRIPT_NAME) infolist = weechat.infolist_get("option", "", "plugins.var.python.%s.layout.*" % SCRIPT_NAME) while weechat.infolist_next(infolist): layout = re.search(pattern, weechat.infolist_string(infolist, "full_name")).groups() if layout[0] not in layouts: layouts.append(layout[0]) weechat.infolist_free(infolist) return layouts def update_layout_list(): """ Updates global LAYOUT_LIST with a sorted array containing layout tuples, ie (layout_name, width, height) """ global LAYOUT_LIST layout_tuples = [] for layout in rlayouts_list(): width = weechat.config_get_plugin("layout.%s.width" % layout) height = weechat.config_get_plugin("layout.%s.height" % layout) if width is not "" and height is not "": layout_tuples.append((layout, int(width), int(height))) layout_tuples.sort(key=itemgetter(1, 2)) _debug("update_layout_list(%s)" % layout_tuples) LAYOUT_LIST = layout_tuples def rlayout_cmd_cb(data, buffer, args): """ Callback for /rlayout command. """ if args == "": weechat.command("", "/help %s" % SCRIPT_COMMAND) return weechat.WEECHAT_RC_OK argv = args.strip().split(" ", 1) if len(argv) == 0: return weechat.WEECHAT_RC_OK short_cmds = ["list", "terminal"] if argv[0] not in short_cmds and len(argv) < 2: _print("Too few arguments for option '%s'." % argv[0]) return weechat.WEECHAT_RC_OK if argv[0] == "size": try: layout, width, height = argv[1].split(" ") if layout_exist(layout): weechat.config_set_plugin("layout.%s.width" % layout, width) weechat.config_set_plugin("layout.%s.height" % layout, height) update_layout_list() else: _print("Layout '%s' doesn't exist, see /help layout to create one." % layout) except ValueError: _print("Too few arguments for option '%s'" % argv[0]) elif argv[0] == "nicklist": try: layout, nicklist = argv[1].split(" ") if layout_exist(layout): if nicklist == "on" or nicklist == "off": weechat.config_set_plugin("layout.%s.nicklist" % layout, nicklist) else: _print("Invalid argument '%s' for option '%s'." % (nicklist, argv[0])) else: _print("Layout '%s' doesn't exist, see /help layout to create one." % layout) except ValueError: _print("Too few arguments for option '%s'" % argv[0]) elif argv[0] == "default_nicklist" or argv[0] == "debug": if argv[1] == "on" or argv[1] == "off": weechat.config_set_plugin(argv[0], argv[1]) else: _print("Invalid argument '%s' for option '%s'" % (argv[1], argv[0])) elif argv[0] == "remove": if argv[1] in rlayouts_list(): for option in ["width", "height", "nicklist"]: weechat.config_unset_plugin("layout.%s.%s" % (argv[1], option)) _print("Removed rlayout '%s'" % argv[1]) else: _print("Could not remove '%s', rlayout not found." % argv[1]) elif argv[0] == "list": if len(rlayouts_list()) == 0: _print("No configuration set.") else: for rlayout in rlayouts_list(): width = weechat.config_get_plugin("layout.%s.width" % rlayout) height = weechat.config_get_plugin("layout.%s.height" % rlayout) nicklist = weechat.config_get_plugin("layout.%s.nicklist" % rlayout) msg = "[%s] width: %s, height: %s" % (rlayout, width, height) if nicklist is not "": msg += ", nicklist: %s" % nicklist _print(msg) elif argv[0] == "terminal": term_height = int(weechat.info_get("term_height", "")) term_width = int(weechat.info_get("term_width", "")) _print("Current terminal width x height is: %s x %s" % (term_width, term_height)) return weechat.WEECHAT_RC_OK def rlayout_bar_cb(data, item, window): infolist = weechat.infolist_get("layout", "", "") layout = None while weechat.infolist_next(infolist): if weechat.infolist_integer(infolist, "current_layout") == 1: layout = weechat.infolist_string(infolist, "name") break weechat.infolist_free(infolist) if layout is None: return "" else: term_height = int(weechat.info_get("term_height", "")) term_width = int(weechat.info_get("term_width", "")) return "%s (%sx%s)" % (layout, term_width, term_height) def rlayout_completion_bool_cb(data, completion_item, buffer, completion): for bool in ("on", "off"): weechat.hook_completion_list_add(completion, bool, 0, weechat.WEECHAT_LIST_POS_SORT) return weechat.WEECHAT_RC_OK def rlayout_completion_layout_list_cb(data, completion_item, buffer, completion): for rlayout in rlayouts_list(): weechat.hook_completion_list_add(completion, rlayout, 0, weechat.WEECHAT_LIST_POS_SORT) return weechat.WEECHAT_RC_OK def config_cb(data, option, value): update_layout_list() return weechat.WEECHAT_RC_OK if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): version = weechat.info_get("version_number", "") or 0 if int(version) < 0x00040300: _print("Requires WeeChat >= 0.4.3 for terminal height and width support.") weechat.command("", "/wait 1ms /python unload %s" % SCRIPT_NAME) weechat.hook_command(SCRIPT_COMMAND, "WeeChat responsive layout configuration", "size || nicklist || default_nicklist " " || remove || list || terminal || debug ", " size: set max size (width and height) for layout to be automatically applied\n" " nicklist: show or hide nicklist bar when layout is automatically applied\n" "default_nicklist: default show or hide nicklist bar if not configured per layout\n" " remove: remove settings for responsive layout\n" " list: list current configuration\n" " terminal: list current terminal width and height\n" " debug: print script debug output\n\n" "To get current layout and terminal dimensions in your bar, use 'rlayout' bar item.", "size %(layouts_names)" " || nicklist %(layouts_names) %(rlayout_bool_value)" " || default_nicklist %(rlayout_bool_value)" " || remove %(rlayouts_names)" " || list" " || terminal" " || debug %(rlayout_bool_value)", "rlayout_cmd_cb", "") # Default settings for option, default_value in SETTINGS.items(): if weechat.config_get_plugin(option) == "": weechat.config_set_plugin(option, default_value[0]) weechat.config_set_desc_plugin(option, '%s (default: %s)' % (default_value[1], default_value[0])) weechat.hook_completion("rlayout_bool_value", "list of bool values", "rlayout_completion_bool_cb", "") weechat.hook_completion("rlayouts_names", "list of rlayouts", "rlayout_completion_layout_list_cb", "") weechat.hook_config("plugins.var.python.responsive_layout.layout.*", "config_cb", "") weechat.bar_item_new("rlayout", "rlayout_bar_cb", "") update_layout_list() hook = weechat.hook_signal("signal_sigwinch", "responsive_cb", "") weechat-scripts/python/bandwidth.py0000644000175100017510000002404515112622376016520 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2009 xt # Copyright (C) 2011 quazgaa # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # #------------------------------------------------------------------- # To make bandwidth monitor visible you need to put "[bandwidth]" # (without "") in your weechat.bar.status.items setting #------------------------------------------------------------------- # # History: # # 2020-09-05, mumixam # version 1.1: add python3 support while still supporting python2 # 2011-12-02, quazgaa # version 1.0: Complete rewrite. Make script more featureful, robust, and accurate. # Thanks to FlashCode and ze for helping debug. # 2011-11-29, quazgaa # version 0.2.1: fixed: take refresh_rate into account for bandwidth calculation # 2009-10-15, xt : # version 0.2: error checking from output command # 2009-10-14, xt : # version 0.1: initial release inspired by nils' perl script # # this is a weechat script try: import weechat except: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") raise SystemExit(0) try: from time import time except: print("Error importing time module.") raise SystemExit(0) try: unichr except NameError: unichr = chr # defines SCRIPT_NAME = "bandwidth" SCRIPT_AUTHOR = "xt " SCRIPT_VERSION = "1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Displays network interface bandwidth (KiB/s and MiB/s) on a bar" SCRIPT_SETTINGS = { "device" : ("eth0", "Network interface(s) to monitor, in order, separated by ';'"), "refresh_rate" : ("5", "Refresh rate in seconds"), "format" : (("%N(" + unichr(8595) + "%DV%DU/s " + unichr(8593) + "%UV%UU/s)"), "Output formatting: %N = network interface, %DV = downstream value, %DU = downstream units (K or M), %UV = upstream value, %UU = upstream units (K or M). Note: default setting uses UTF-8"), "separator" : (" ", "String displayed between output for multiple devices"), } STATS_FILE = "/proc/net/dev" # global variables last_device = [] last_down_bytes = [] last_up_bytes = [] last_time = 0 def main(): if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): version = int(weechat.info_get('version_number', '')) or 0 # unset unused setting from older versions of script if weechat.config_is_set_plugin('display_unit'): weechat.prnt("", "Option plugins.var.python.bandwidth.display_unit no longer used, removing.") weechat.config_unset_plugin('display_unit') # set default settings for option in SCRIPT_SETTINGS.keys(): if not weechat.config_is_set_plugin(option): value = SCRIPT_SETTINGS[option][0] if isinstance(value, str): pass elif isinstance(value, bytes): pass elif isinstance(value, unicode): value = value.encode('utf8') weechat.config_set_plugin(option, value) if version >= 0x00030500: weechat.config_set_desc_plugin(option, SCRIPT_SETTINGS[option][1]) # ensure sane refresh_rate setting if int(weechat.config_get_plugin('refresh_rate')) < 1: weechat.prnt("", "{}Invalid value for option plugins.var.python.bandwidth.refresh_rate, setting to default of {}".format(weechat.prefix("error"), SCRIPT_SETTINGS['refresh_rate'][0])) weechat.config_set_plugin('refresh_rate', SCRIPT_SETTINGS['refresh_rate'][0]) # create the bandwidth monitor bar item weechat.bar_item_new('bandwidth', 'bandwidth_item_cb', '') # update it every plugins.var.python.bandwidth.refresh_rate seconds weechat.hook_timer(int(weechat.config_get_plugin('refresh_rate'))*1000, 0, 0, 'bandwidth_timer_cb', '') def bandwidth_timer_cb(data, remaining_calls): weechat.bar_item_update('bandwidth') return weechat.WEECHAT_RC_OK def bandwidth_item_cb(data, buffer, args): global last_device, last_down_bytes, last_up_bytes, last_time device = weechat.config_get_plugin('device').strip(';').split(';') output_format = weechat.config_get_plugin('format') separator = weechat.config_get_plugin('separator') invalid_settings = False # ensure sane settings if not device[0]: weechat.prnt("", "{}Option plugins.var.python.bandwidth.device should contain at least one device name, setting to default of {}".format(weechat.prefix("error"), SCRIPT_SETTINGS['device'][0])) weechat.config_set_plugin('device', SCRIPT_SETTINGS['device'][0]) invalid_settings = True if int(weechat.config_get_plugin('refresh_rate')) < 1: weechat.prnt("", "{}Invalid value for option plugins.var.python.bandwidth.refresh_rate, setting to default of {}".format(weechat.prefix("error"), SCRIPT_SETTINGS['refresh_rate'][0])) weechat.config_set_plugin('refresh_rate', SCRIPT_SETTINGS['refresh_rate'][0]) invalid_settings = True if '%DV' not in output_format and '%UV' not in output_format: weechat.prnt("", "{}Option plugins.var.python.bandwidth.format should contain at least one of: '%DV' or '%UV'. Setting to default of '{}'".format(weechat.prefix("error"), SCRIPT_SETTINGS['format'][0])) weechat.config_set_plugin('format', SCRIPT_SETTINGS['format'][0]) invalid_settings = True if invalid_settings: return '' # open the network device status information file try: f = open(STATS_FILE) except: weechat.prnt("", "{}Error opening {}".format(weechat.prefix("error"), STATS_FILE)) return '' else: current_time = time() try: foo = f.read() except: weechat.prnt("", "{}Error reading {}".format(weechat.prefix("error"), STATS_FILE)) f.close() return '' f.close() current_down_bytes = [] current_up_bytes = [] num_devices = len(device) num_last_devices = len(last_device) lines = foo.splitlines() new_device_list = False device_exist = False # get the downstream and upstream byte counts for i in range(num_devices): for line in lines: if (device[i] + ':') in line: field = line.split(':')[1].strip().split() current_down_bytes.append(float(field[0])) current_up_bytes.append(float(field[8])) device_exist = True break if device_exist: device_exist = False else: current_down_bytes.append(0) current_up_bytes.append(0) # check if the set of network devices to monitor has changed while script is running, if last_device: if num_last_devices != num_devices: new_device_list = True else: for i in range(num_devices): if device[i] != last_device[i]: new_device_list = True break # if so, clear the global variables, if new_device_list: del last_device[:] del last_down_bytes[:] del last_up_bytes[:] # set them afresh (also if script first starting), if not last_device: if num_devices: for i in range(num_devices): last_device.append(device[i]) last_down_bytes.append(current_down_bytes[i]) last_up_bytes.append(current_up_bytes[i]) last_time = current_time # and start from the beginning return '' # calculate downstream and upstream rates in KiB/s if num_devices: down_rate = [] up_rate = [] time_elapsed = current_time - last_time last_time = current_time for i in range(num_devices): down_rate.append((current_down_bytes[i] - last_down_bytes[i]) / time_elapsed / 1024) up_rate.append((current_up_bytes[i] - last_up_bytes[i]) / time_elapsed / 1024) last_down_bytes[i] = current_down_bytes[i] last_up_bytes[i] = current_up_bytes[i] output_item = [output_format for i in device] output = '' # determine downstream and upstream units; format the output for i in range(num_devices): if '%DU' in output_item[i]: if down_rate[i] >= 1024: down_rate[i] = round((down_rate[i]/1024), 1) down_rate_unit = 'M' else: down_rate[i] = int(round(down_rate[i])) down_rate_unit = 'K' output_item[i] = output_item[i].replace('%DU', down_rate_unit) if '%UU' in output_item[i]: if up_rate[i] >= 1024: up_rate[i] = round((up_rate[i]/1024), 1) up_rate_unit = 'M' else: up_rate[i] = int(round(up_rate[i])) up_rate_unit = 'K' output_item[i] = output_item[i].replace('%UU', up_rate_unit) output_item[i] = output_item[i].replace('%DV', str(down_rate[i])) output_item[i] = output_item[i].replace('%UV', str(up_rate[i])) output_item[i] = output_item[i].replace('%N', device[i]) if output: output += separator + output_item[i] else: output = output_item[i] # return the result return output if __name__ == "__main__": main() weechat-scripts/python/vimode.py0000644000175100017510000021024115112622404016022 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2013-2014 Germain Z. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Add vi/vim-like modes to WeeChat. # import csv import json import os import re import subprocess try: from StringIO import StringIO except ImportError: from io import StringIO import time import weechat # Script info. # ============ SCRIPT_NAME = "vimode" SCRIPT_AUTHOR = "GermainZ " SCRIPT_VERSION = "0.8.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = ("Add vi/vim-like modes and keybindings to WeeChat.") # Global variables. # ================= # General. # -------- # Halp! Halp! Halp! GITHUB_BASE = "https://github.com/GermainZ/weechat-vimode/blob/master/" README_URL = GITHUB_BASE + "README.md" FAQ_KEYBINDINGS = GITHUB_BASE + "FAQ.md#problematic-key-bindings" FAQ_ESC = GITHUB_BASE + "FAQ.md#esc-key-not-being-detected-instantly" # Holds the text of the tab-completions for the command-line mode. cmd_compl_text = "" # Holds the original text of the command-line mode, used for completion. cmd_text_orig = None # Index of current suggestion, used for completion. cmd_compl_pos = 0 # Used for command-line mode history. cmd_history = [] cmd_history_index = 0 # Used to store the content of the input line when going into COMMAND mode. input_line_backup = {} # Mode we're in. One of INSERT, NORMAL, REPLACE, COMMAND or SEARCH. # SEARCH is only used if search_vim is enabled. mode = "INSERT" # Holds normal commands (e.g. "dd"). vi_buffer = "" # See `cb_key_combo_default()`. esc_pressed = 0 # See `cb_key_pressed()`. last_signal_time = 0 # See `start_catching_keys()` for more info. catching_keys_data = {'amount': 0} # Used for ; and , to store the last f/F/t/T motion. last_search_motion = {'motion': None, 'data': None} # Used for undo history. undo_history = {} undo_history_index = {} # Holds mode colors (loaded from vimode_settings). mode_colors = {} # Script options. vimode_settings = { 'no_warn': ("off", ("don't warn about problematic keybindings and " "tmux/screen")), 'copy_clipboard_cmd': ("xclip -selection c", ("command used to copy to clipboard; must read " "input from stdin")), 'paste_clipboard_cmd': ("xclip -selection c -o", ("command used to paste clipboard; must output " "content to stdout")), 'imap_esc': ("", ("use alternate mapping to enter Normal mode while in " "Insert mode; having it set to 'jk' is similar to " "`:imap jk ` in vim")), 'imap_esc_timeout': ("1000", ("time in ms to wait for the imap_esc " "sequence to complete")), 'search_vim': ("off", ("allow n/N usage after searching (requires an extra" " to return to normal mode)")), 'user_mappings': ("", ("see the `:nmap` command in the README for more " "info; please do not modify this field manually " "unless you know what you're doing")), 'mode_indicator_prefix': ("", "prefix for the bar item mode_indicator"), 'mode_indicator_suffix': ("", "suffix for the bar item mode_indicator"), 'mode_indicator_normal_color': ("white", "color for mode indicator in Normal mode"), 'mode_indicator_normal_color_bg': ("gray", ("background color for mode indicator " "in Normal mode")), 'mode_indicator_insert_color': ("white", "color for mode indicator in Insert mode"), 'mode_indicator_insert_color_bg': ("blue", ("background color for mode indicator " "in Insert mode")), 'mode_indicator_replace_color': ("white", "color for mode indicator in Replace mode"), 'mode_indicator_replace_color_bg': ("red", ("background color for mode indicator " "in Replace mode")), 'mode_indicator_cmd_color': ("white", "color for mode indicator in Command mode"), 'mode_indicator_cmd_color_bg': ("cyan", ("background color for mode indicator in " "Command mode")), 'mode_indicator_search_color': ("white", "color for mode indicator in Search mode"), 'mode_indicator_search_color_bg': ("magenta", ("background color for mode indicator " "in Search mode")), 'line_number_prefix': ("", "prefix for line numbers"), 'line_number_suffix': (" ", "suffix for line numbers") } # Regex patterns. # --------------- WHITESPACE = re.compile(r"\s") IS_KEYWORD = re.compile(r"[a-zA-Z0-9_@À-ÿ]") REGEX_MOTION_LOWERCASE_W = re.compile(r"\b\S|(?<=\s)\S") REGEX_MOTION_UPPERCASE_W = re.compile(r"(?<=\s)\S") REGEX_MOTION_UPPERCASE_E = re.compile(r"\S(?!\S)") REGEX_MOTION_UPPERCASE_B = REGEX_MOTION_UPPERCASE_E REGEX_MOTION_G_UPPERCASE_E = REGEX_MOTION_UPPERCASE_W REGEX_MOTION_CARRET = re.compile(r"\S") REGEX_INT = r"[0-9]" REGEX_MAP_KEYS_1 = { re.compile("<([^>]*-)Left>", re.IGNORECASE): '<\\1\x01[[D>', re.compile("<([^>]*-)Right>", re.IGNORECASE): '<\\1\x01[[C>', re.compile("<([^>]*-)Up>", re.IGNORECASE): '<\\1\x01[[A>', re.compile("<([^>]*-)Down>", re.IGNORECASE): '<\\1\x01[[B>', re.compile("", re.IGNORECASE): '\x01[[D', re.compile("", re.IGNORECASE): '\x01[[C', re.compile("", re.IGNORECASE): '\x01[[A', re.compile("", re.IGNORECASE): '\x01[[B' } REGEX_MAP_KEYS_2 = { re.compile(r"]*)>", re.IGNORECASE): '\x01\\1', re.compile(r"]*)>", re.IGNORECASE): '\x01[\\1' } # Regex used to detect problematic keybindings. # For example: meta-wmeta-s is bound by default to ``/window swap``. # If the user pressed Esc-w, WeeChat will detect it as meta-w and will not # send any signal to `cb_key_combo_default()` just yet, since it's the # beginning of a known key combo. # Instead, `cb_key_combo_default()` will receive the Esc-ws signal, which # becomes "ws" after removing the Esc part, and won't know how to handle it. REGEX_PROBLEMATIC_KEYBINDINGS = re.compile(r"meta-\w(meta|ctrl)") # Vi commands. # ------------ def cmd_nmap(args): """Add a user-defined key mapping. Some (but not all) vim-like key codes are supported to simplify things for the user: , , , , and . See Also: `cmd_unmap()`. """ args = args.strip() if not args: mappings = vimode_settings['user_mappings'] if mappings: weechat.prnt("", "User-defined key mappings:") for key, mapping in mappings.items(): weechat.prnt("", "{} -> {}".format(key, mapping)) else: weechat.prnt("", "nmap: no mapping found.") elif not " " in args: weechat.prnt("", "nmap syntax -> :nmap {lhs} {rhs}") else: key, mapping = args.split(" ", 1) # First pass of replacements. We perform two passes as a simple way to # avoid incorrect replacements due to dictionaries not being # insertion-ordered prior to Python 3.7. for regex, repl in REGEX_MAP_KEYS_1.items(): key = regex.sub(repl, key) mapping = regex.sub(repl, mapping) # Second pass of replacements. for regex, repl in REGEX_MAP_KEYS_2.items(): key = regex.sub(repl, key) mapping = regex.sub(repl, mapping) mappings = vimode_settings['user_mappings'] mappings[key] = mapping weechat.config_set_plugin('user_mappings', json.dumps(mappings)) vimode_settings['user_mappings'] = mappings def cmd_nunmap(args): """Remove a user-defined key mapping. See Also: `cmd_map()`. """ args = args.strip() if not args: weechat.prnt("", "nunmap syntax -> :unmap {lhs}") else: key = args for regex, repl in REGEX_MAP_KEYS_1.items(): key = regex.sub(repl, key) for regex, repl in REGEX_MAP_KEYS_2.items(): key = regex.sub(repl, key) mappings = vimode_settings['user_mappings'] if key in mappings: del mappings[key] weechat.config_set_plugin('user_mappings', json.dumps(mappings)) vimode_settings['user_mappings'] = mappings else: weechat.prnt("", "nunmap: No such mapping") # See Also: `cb_exec_cmd()`. VI_COMMAND_GROUPS = {('h', 'help'): "/help", ('qa', 'qall', 'quita', 'quitall'): "/exit", ('q', 'quit'): "/close", ('w', 'write'): "/save", ('bN', 'bNext', 'bp', 'bprevious'): "/buffer -1", ('bn', 'bnext'): "/buffer +1", ('bd', 'bdel', 'bdelete'): "/close", ('b#',): "/input jump_last_buffer_displayed", ('b', 'bu', 'buf', 'buffer'): "/buffer", ('sp', 'split'): "/window splith", ('vs', 'vsplit'): "/window splitv", ('nm', 'nmap'): cmd_nmap, ('nun', 'nunmap'): cmd_nunmap} VI_COMMANDS = dict() for T, v in VI_COMMAND_GROUPS.items(): VI_COMMANDS.update(dict.fromkeys(T, v)) # Vi operators. # ------------- # Each operator must have a corresponding function, called "operator_X" where # X is the operator. For example: `operator_c()`. VI_OPERATORS = ["c", "d", "y"] # Vi motions. # ----------- # Vi motions. Each motion must have a corresponding function, called # "motion_X" where X is the motion (e.g. `motion_w()`). # See Also: `SPECIAL_CHARS`. VI_MOTIONS = ["w", "e", "b", "^", "$", "h", "l", "W", "E", "B", "f", "F", "t", "T", "ge", "gE", "0"] # Special characters for motions. The corresponding function's name is # converted before calling. For example, "^" will call `motion_carret` instead # of `motion_^` (which isn't allowed because of illegal characters). SPECIAL_CHARS = {'^': "carret", '$': "dollar"} # Methods for vi operators, motions and key bindings. # =================================================== # Documented base examples: # ------------------------- def operator_base(buf, input_line, pos1, pos2, overwrite): """Operator method example. Args: buf (str): pointer to the current WeeChat buffer. input_line (str): the content of the input line. pos1 (int): the starting position of the motion. pos2 (int): the ending position of the motion. overwrite (bool, optional): whether the character at the cursor's new position should be overwritten or not (for inclusive motions). Defaults to False. Notes: Should be called "operator_X", where X is the operator, and defined in `VI_OPERATORS`. Must perform actions (e.g. modifying the input line) on its own, using the WeeChat API. See Also: For additional examples, see `operator_d()` and `operator_y()`. """ # Get start and end positions. start = min(pos1, pos2) end = max(pos1, pos2) # Print the text the operator should go over. weechat.prnt("", "Selection: %s" % input_line[start:end]) def motion_base(input_line, cur, count): """Motion method example. Args: input_line (str): the content of the input line. cur (int): the position of the cursor. count (int): the amount of times to multiply or iterate the action. Returns: A tuple containing three values: int: the new position of the cursor. bool: True if the motion is inclusive, False otherwise. bool: True if the motion is catching, False otherwise. See `start_catching_keys()` for more info on catching motions. Notes: Should be called "motion_X", where X is the motion, and defined in `VI_MOTIONS`. Must not modify the input line directly. See Also: For additional examples, see `motion_w()` (normal motion) and `motion_f()` (catching motion). """ # Find (relative to cur) position of next number. pos = get_pos(input_line, REGEX_INT, cur, True, count) # Return the new (absolute) cursor position. # This motion is exclusive, so overwrite is False. return cur + pos, False def key_base(buf, input_line, cur, count): """Key method example. Args: buf (str): pointer to the current WeeChat buffer. input_line (str): the content of the input line. cur (int): the position of the cursor. count (int): the amount of times to multiply or iterate the action. Notes: Should be called `key_X`, where X represents the key(s), and defined in `VI_KEYS`. Must perform actions on its own (using the WeeChat API). See Also: For additional examples, see `key_a()` (normal key) and `key_r()` (catching key). """ # Key was pressed. Go to Insert mode (similar to "i"). set_mode("INSERT") # Operators: # ---------- def operator_d(buf, input_line, pos1, pos2, overwrite=False): """Delete text from `pos1` to `pos2` from the input line. If `overwrite` is set to True, the character at the cursor's new position is removed as well (the motion is inclusive). See Also: `operator_base()`. """ start = min(pos1, pos2) end = max(pos1, pos2) if overwrite: end += 1 input_line = list(input_line) del input_line[start:end] input_line = "".join(input_line) weechat.buffer_set(buf, "input", input_line) set_cur(buf, input_line, pos1) def operator_c(buf, input_line, pos1, pos2, overwrite=False): """Delete text from `pos1` to `pos2` from the input and enter Insert mode. If `overwrite` is set to True, the character at the cursor's new position is removed as well (the motion is inclusive.) See Also: `operator_base()`. """ operator_d(buf, input_line, pos1, pos2, overwrite) set_mode("INSERT") def operator_y(buf, input_line, pos1, pos2, _): """Yank text from `pos1` to `pos2` from the input line. See Also: `operator_base()`. """ start = min(pos1, pos2) end = max(pos1, pos2) cmd = vimode_settings['copy_clipboard_cmd'] proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE) proc.communicate(input=input_line[start:end].encode()) # Motions: # -------- def motion_0(input_line, cur, count): """Go to the first character of the line. See Also; `motion_base()`. """ return 0, False, False def motion_w(input_line, cur, count): """Go `count` words forward and return position. See Also: `motion_base()`. """ pos = get_pos(input_line, REGEX_MOTION_LOWERCASE_W, cur, True, count) if pos == -1: return len(input_line), False, False return cur + pos, False, False def motion_W(input_line, cur, count): """Go `count` WORDS forward and return position. See Also: `motion_base()`. """ pos = get_pos(input_line, REGEX_MOTION_UPPERCASE_W, cur, True, count) if pos == -1: return len(input_line), False, False return cur + pos, False, False def motion_e(input_line, cur, count): """Go to the end of `count` words and return position. See Also: `motion_base()`. """ for _ in range(max(1, count)): found = False pos = cur for pos in range(cur + 1, len(input_line) - 1): # Whitespace, keep going. if WHITESPACE.match(input_line[pos]): pass # End of sequence made from 'iskeyword' characters only, # or end of sequence made from non 'iskeyword' characters only. elif ((IS_KEYWORD.match(input_line[pos]) and (not IS_KEYWORD.match(input_line[pos + 1]) or WHITESPACE.match(input_line[pos + 1]))) or (not IS_KEYWORD.match(input_line[pos]) and (IS_KEYWORD.match(input_line[pos + 1]) or WHITESPACE.match(input_line[pos + 1])))): found = True cur = pos break # We're at the character before the last and we still found nothing. # Go to the last character. if not found: cur = pos + 1 return cur, True, False def motion_E(input_line, cur, count): """Go to the end of `count` WORDS and return cusor position. See Also: `motion_base()`. """ pos = get_pos(input_line, REGEX_MOTION_UPPERCASE_E, cur, True, count) if pos == -1: return len(input_line), False, False return cur + pos, True, False def motion_b(input_line, cur, count): """Go `count` words backwards and return position. See Also: `motion_base()`. """ # "b" is just "e" on inverted data (e.g. "olleH" instead of "Hello"). pos_inv = motion_e(input_line[::-1], len(input_line) - cur - 1, count)[0] pos = len(input_line) - pos_inv - 1 return pos, True, False def motion_B(input_line, cur, count): """Go `count` WORDS backwards and return position. See Also: `motion_base()`. """ new_cur = len(input_line) - cur pos = get_pos(input_line[::-1], REGEX_MOTION_UPPERCASE_B, new_cur, count=count) if pos == -1: return 0, False, False pos = len(input_line) - (pos + new_cur + 1) return pos, True, False def motion_ge(input_line, cur, count): """Go to end of `count` words backwards and return position. See Also: `motion_base()`. """ # "ge is just "w" on inverted data (e.g. "olleH" instead of "Hello"). pos_inv = motion_w(input_line[::-1], len(input_line) - cur - 1, count)[0] pos = len(input_line) - pos_inv - 1 return pos, True, False def motion_gE(input_line, cur, count): """Go to end of `count` WORDS backwards and return position. See Also: `motion_base()`. """ new_cur = len(input_line) - cur - 1 pos = get_pos(input_line[::-1], REGEX_MOTION_G_UPPERCASE_E, new_cur, True, count) if pos == -1: return 0, False, False pos = len(input_line) - (pos + new_cur + 1) return pos, True, False def motion_h(input_line, cur, count): """Go `count` characters to the left and return position. See Also: `motion_base()`. """ return max(0, cur - max(count, 1)), False, False def motion_l(input_line, cur, count): """Go `count` characters to the right and return position. See Also: `motion_base()`. """ return cur + max(count, 1), False, False def motion_carret(input_line, cur, count): """Go to first non-blank character of line and return position. See Also: `motion_base()`. """ pos = get_pos(input_line, REGEX_MOTION_CARRET, 0) return pos, False, False def motion_dollar(input_line, cur, count): """Go to end of line and return position. See Also: `motion_base()`. """ pos = len(input_line) return pos, False, False def motion_f(input_line, cur, count): """Go to `count`'th occurence of character and return position. See Also: `motion_base()`. """ return start_catching_keys(1, "cb_motion_f", input_line, cur, count) def cb_motion_f(update_last=True): """Callback for `motion_f()`. Args: update_last (bool, optional): should `last_search_motion` be updated? Set to False when calling from `key_semicolon()` or `key_comma()` so that the last search motion isn't overwritten. Defaults to True. See Also: `start_catching_keys()`. """ global last_search_motion pattern = catching_keys_data['keys'] pos = get_pos(catching_keys_data['input_line'], re.escape(pattern), catching_keys_data['cur'], True, catching_keys_data['count']) catching_keys_data['new_cur'] = max(0, pos) + catching_keys_data['cur'] if update_last: last_search_motion = {'motion': "f", 'data': pattern} cb_key_combo_default(None, None, "") def motion_F(input_line, cur, count): """Go to `count`'th occurence of char to the right and return position. See Also: `motion_base()`. """ return start_catching_keys(1, "cb_motion_F", input_line, cur, count) def cb_motion_F(update_last=True): """Callback for `motion_F()`. Args: update_last (bool, optional): should `last_search_motion` be updated? Set to False when calling from `key_semicolon()` or `key_comma()` so that the last search motion isn't overwritten. Defaults to True. See Also: `start_catching_keys()`. """ global last_search_motion pattern = catching_keys_data['keys'] cur = len(catching_keys_data['input_line']) - catching_keys_data['cur'] pos = get_pos(catching_keys_data['input_line'][::-1], re.escape(pattern), cur, False, catching_keys_data['count']) catching_keys_data['new_cur'] = catching_keys_data['cur'] - max(0, pos + 1) if update_last: last_search_motion = {'motion': "F", 'data': pattern} cb_key_combo_default(None, None, "") def motion_t(input_line, cur, count): """Go to `count`'th occurence of char and return position. The position returned is the position of the character to the left of char. See Also: `motion_base()`. """ return start_catching_keys(1, "cb_motion_t", input_line, cur, count) def cb_motion_t(update_last=True): """Callback for `motion_t()`. Args: update_last (bool, optional): should `last_search_motion` be updated? Set to False when calling from `key_semicolon()` or `key_comma()` so that the last search motion isn't overwritten. Defaults to True. See Also: `start_catching_keys()`. """ global last_search_motion pattern = catching_keys_data['keys'] pos = get_pos(catching_keys_data['input_line'], re.escape(pattern), catching_keys_data['cur'] + 1, True, catching_keys_data['count']) pos += 1 if pos > 0: catching_keys_data['new_cur'] = pos + catching_keys_data['cur'] - 1 else: catching_keys_data['new_cur'] = catching_keys_data['cur'] if update_last: last_search_motion = {'motion': "t", 'data': pattern} cb_key_combo_default(None, None, "") def motion_T(input_line, cur, count): """Go to `count`'th occurence of char to the left and return position. The position returned is the position of the character to the right of char. See Also: `motion_base()`. """ return start_catching_keys(1, "cb_motion_T", input_line, cur, count) def cb_motion_T(update_last=True): """Callback for `motion_T()`. Args: update_last (bool, optional): should `last_search_motion` be updated? Set to False when calling from `key_semicolon()` or `key_comma()` so that the last search motion isn't overwritten. Defaults to True. See Also: `start_catching_keys()`. """ global last_search_motion pattern = catching_keys_data['keys'] pos = get_pos(catching_keys_data['input_line'][::-1], re.escape(pattern), (len(catching_keys_data['input_line']) - (catching_keys_data['cur'] + 1)) + 1, True, catching_keys_data['count']) pos += 1 if pos > 0: catching_keys_data['new_cur'] = catching_keys_data['cur'] - pos + 1 else: catching_keys_data['new_cur'] = catching_keys_data['cur'] if update_last: last_search_motion = {'motion': "T", 'data': pattern} cb_key_combo_default(None, None, "") # Keys: # ----- def key_cc(buf, input_line, cur, count): """Delete line and start Insert mode. See Also: `key_base()`. """ weechat.command("", "/input delete_line") set_mode("INSERT") def key_C(buf, input_line, cur, count): """Delete from cursor to end of line and start Insert mode. See Also: `key_base()`. """ weechat.command("", "/input delete_end_of_line") set_mode("INSERT") def key_yy(buf, input_line, cur, count): """Yank line. See Also: `key_base()`. """ cmd = vimode_settings['copy_clipboard_cmd'] proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE) proc.communicate(input=input_line.encode()) def key_p(buf, input_line, cur, count): """Paste text. See Also: `key_base()`. """ cmd = vimode_settings['paste_clipboard_cmd'] weechat.hook_process(cmd, 10 * 1000, "cb_key_p", weechat.current_buffer()) def cb_key_p(data, command, return_code, output, err): """Callback for fetching clipboard text and pasting it.""" buf = "" this_buffer = data if output != "": buf += output.strip() if return_code == 0: my_input = weechat.buffer_get_string(this_buffer, "input") pos = weechat.buffer_get_integer(this_buffer, "input_pos") my_input = my_input[:pos] + buf + my_input[pos:] pos += len(buf) weechat.buffer_set(this_buffer, "input", my_input) weechat.buffer_set(this_buffer, "input_pos", str(pos)) return weechat.WEECHAT_RC_OK def key_i(buf, input_line, cur, count): """Start Insert mode. See Also: `key_base()`. """ set_mode("INSERT") def key_a(buf, input_line, cur, count): """Move cursor one character to the right and start Insert mode. See Also: `key_base()`. """ set_cur(buf, input_line, cur + 1, False) set_mode("INSERT") def key_A(buf, input_line, cur, count): """Move cursor to end of line and start Insert mode. See Also: `key_base()`. """ set_cur(buf, input_line, len(input_line), False) set_mode("INSERT") def key_I(buf, input_line, cur, count): """Move cursor to first non-blank character and start Insert mode. See Also: `key_base()`. """ pos, _, _ = motion_carret(input_line, cur, 0) set_cur(buf, input_line, pos) set_mode("INSERT") def key_G(buf, input_line, cur, count): """Scroll to specified line or bottom of buffer. See Also: `key_base()`. """ if count > 0: # This is necessary to prevent weird scroll jumps. weechat.command("", "/window scroll_top") weechat.command("", "/window scroll %s" % (count - 1)) else: weechat.command("", "/window scroll_bottom") def key_r(buf, input_line, cur, count): """Replace `count` characters under the cursor. See Also: `key_base()`. """ start_catching_keys(1, "cb_key_r", input_line, cur, count, buf) def cb_key_r(): """Callback for `key_r()`. See Also: `start_catching_keys()`. """ global catching_keys_data input_line = list(catching_keys_data['input_line']) count = max(catching_keys_data['count'], 1) cur = catching_keys_data['cur'] if cur + count <= len(input_line): for _ in range(count): input_line[cur] = catching_keys_data['keys'] cur += 1 input_line = "".join(input_line) weechat.buffer_set(catching_keys_data['buf'], "input", input_line) set_cur(catching_keys_data['buf'], input_line, cur - 1) catching_keys_data = {'amount': 0} def key_R(buf, input_line, cur, count): """Start Replace mode. See Also: `key_base()`. """ set_mode("REPLACE") def key_tilda(buf, input_line, cur, count): """Switch the case of `count` characters under the cursor. See Also: `key_base()`. """ input_line = list(input_line) count = max(1, count) while count and cur < len(input_line): input_line[cur] = input_line[cur].swapcase() count -= 1 cur += 1 input_line = "".join(input_line) weechat.buffer_set(buf, "input", input_line) set_cur(buf, input_line, cur) def key_alt_j(buf, input_line, cur, count): """Go to WeeChat buffer. Called to preserve WeeChat's alt-j buffer switching. This is only called when alt-j is pressed after pressing Esc, because \x01\x01j is received in key_combo_default which becomes \x01j after removing the detected Esc key. If Esc isn't the last pressed key, \x01j is directly received in key_combo_default. """ start_catching_keys(2, "cb_key_alt_j", input_line, cur, count) def cb_key_alt_j(): """Callback for `key_alt_j()`. See Also: `start_catching_keys()`. """ global catching_keys_data weechat.command("", "/buffer " + catching_keys_data['keys']) catching_keys_data = {'amount': 0} def key_semicolon(buf, input_line, cur, count, swap=False): """Repeat last f, t, F, T `count` times. Args: swap (bool, optional): if True, the last motion will be repeated in the opposite direction (e.g. "f" instead of "F"). Defaults to False. See Also: `key_base()`. """ global catching_keys_data, vi_buffer catching_keys_data = ({'amount': 0, 'input_line': input_line, 'cur': cur, 'keys': last_search_motion['data'], 'count': count, 'new_cur': 0, 'buf': buf}) # Swap the motion's case if called from key_comma. if swap: motion = last_search_motion['motion'].swapcase() else: motion = last_search_motion['motion'] func = "cb_motion_%s" % motion vi_buffer = motion globals()[func](False) def key_comma(buf, input_line, cur, count): """Repeat last f, t, F, T in opposite direction `count` times. See Also: `key_base()`. """ key_semicolon(buf, input_line, cur, count, True) def key_u(buf, input_line, cur, count): """Undo change `count` times. See Also: `key_base()`. """ buf = weechat.current_buffer() if buf not in undo_history: return for _ in range(max(count, 1)): if undo_history_index[buf] > -len(undo_history[buf]): undo_history_index[buf] -= 1 input_line = undo_history[buf][undo_history_index[buf]] weechat.buffer_set(buf, "input", input_line) else: break def key_ctrl_r(buf, input_line, cur, count): """Redo change `count` times. See Also: `key_base()`. """ if buf not in undo_history: return for _ in range(max(count, 1)): if undo_history_index[buf] < -1: undo_history_index[buf] += 1 input_line = undo_history[buf][undo_history_index[buf]] weechat.buffer_set(buf, "input", input_line) else: break # Vi key bindings. # ================ # String values will be executed as normal WeeChat commands. # For functions, see `key_base()` for reference. VI_KEYS = {'j': "/window scroll_down", 'k': "/window scroll_up", 'G': key_G, 'gg': "/window scroll_top", 'x': "/input delete_next_char", 'X': "/input delete_previous_char", 'dd': "/input delete_line", 'D': "/input delete_end_of_line", 'cc': key_cc, 'C': key_C, 'i': key_i, 'a': key_a, 'A': key_A, 'I': key_I, 'yy': key_yy, 'p': key_p, 'gt': "/buffer -1", 'K': "/buffer -1", 'gT': "/buffer +1", 'J': "/buffer +1", 'r': key_r, 'R': key_R, '~': key_tilda, 'nt': "/bar scroll nicklist * -100%", 'nT': "/bar scroll nicklist * +100%", '\x01[[A': "/input history_previous", '\x01[[B': "/input history_next", '\x01[[C': "/input move_next_char", '\x01[[D': "/input move_previous_char", '\x01[[H': "/input move_beginning_of_line", '\x01[[F': "/input move_end_of_line", '\x01[[5~': "/window page_up", '\x01[[6~': "/window page_down", '\x01[[3~': "/input delete_next_char", '\x01[[2~': key_i, '\x01M': "/input return", '\x01?': "/input move_previous_char", ' ': "/input move_next_char", '\x01[j': key_alt_j, '\x01[1': "/buffer *1", '\x01[2': "/buffer *2", '\x01[3': "/buffer *3", '\x01[4': "/buffer *4", '\x01[5': "/buffer *5", '\x01[6': "/buffer *6", '\x01[7': "/buffer *7", '\x01[8': "/buffer *8", '\x01[9': "/buffer *9", '\x01[0': "/buffer *10", '\x01^': "/input jump_last_buffer_displayed", '\x01D': "/window page_down", '\x01U': "/window page_up", '\x01Wh': "/window left", '\x01Wj': "/window down", '\x01Wk': "/window up", '\x01Wl': "/window right", '\x01W=': "/window balance", '\x01Wx': "/window swap", '\x01Ws': "/window splith", '\x01Wv': "/window splitv", '\x01Wq': "/window merge", ';': key_semicolon, ',': key_comma, 'u': key_u, '\x01R': key_ctrl_r} # Add alt-j bindings. for i in range(10, 99): VI_KEYS['\x01[j%s' % i] = "/buffer %s" % i # Key handling. # ============= def cb_key_pressed(data, signal, signal_data): """Detect potential Esc presses. Alt and Esc are detected as the same key in most terminals. The difference is that Alt signal is sent just before the other pressed key's signal. We therefore use a timeout (50ms) to detect whether Alt or Esc was pressed. """ global last_signal_time last_signal_time = time.time() if signal_data == "\x01[": # In 50ms, check if any other keys were pressed. If not, it's Esc! weechat.hook_timer(50, 0, 1, "cb_check_esc", "{:f}".format(last_signal_time)) return weechat.WEECHAT_RC_OK def cb_check_esc(data, remaining_calls): """Check if the Esc key was pressed and change the mode accordingly.""" global esc_pressed, vi_buffer, catching_keys_data # Not perfect, would be better to use direct comparison (==) but that only # works for py2 and not for py3. if abs(last_signal_time - float(data)) <= 0.000001: esc_pressed += 1 if mode == "SEARCH": weechat.command("", "/input search_stop_here") set_mode("NORMAL") # Cancel any current partial commands. vi_buffer = "" catching_keys_data = {'amount': 0} weechat.bar_item_update("vi_buffer") return weechat.WEECHAT_RC_OK def cb_key_combo_default(data, signal, signal_data): """Eat and handle key events when in Normal mode, if needed. The key_combo_default signal is sent when a key combo is pressed. For example, alt-k will send the "\x01[k" signal. Esc is handled a bit differently to avoid delays, see `cb_key_pressed()`. """ global esc_pressed, vi_buffer, cmd_compl_text, cmd_text_orig, \ cmd_compl_pos, cmd_history_index # If Esc was pressed, strip the Esc part from the pressed keys. # Example: user presses Esc followed by i. This is detected as "\x01[i", # but we only want to handle "i". keys = signal_data if esc_pressed or esc_pressed == -2: if keys.startswith("\x01[" * esc_pressed): # Multiples of 3 seem to "cancel" themselves, # e.g. Esc-Esc-Esc-Alt-j-11 is detected as "\x01[\x01[\x01" # followed by "\x01[j11" (two different signals). if signal_data == "\x01[" * 3: esc_pressed = -1 # `cb_check_esc()` will increment it to 0. else: esc_pressed = 0 # This can happen if a valid combination is started but interrupted # with Esc, such as Ctrl-W→Esc→w which would send two signals: # "\x01W\x01[" then "\x01W\x01[w". # In that case, we still need to handle the next signal ("\x01W\x01[w") # so we use the special value "-2". else: esc_pressed = -2 keys = keys.split("\x01[")[-1] # Remove the "Esc" part(s). # Ctrl-Space. elif keys == "\x01@": set_mode("NORMAL") return weechat.WEECHAT_RC_OK_EAT # Clear the undo history for this buffer on . if keys == "\x01M": buf = weechat.current_buffer() clear_undo_history(buf) # Detect imap_esc presses if any. if mode == "INSERT": imap_esc = vimode_settings['imap_esc'] if not imap_esc: return weechat.WEECHAT_RC_OK if (imap_esc.startswith(vi_buffer) and imap_esc[len(vi_buffer):len(vi_buffer)+1] == keys): vi_buffer += keys weechat.bar_item_update("vi_buffer") weechat.hook_timer(int(vimode_settings['imap_esc_timeout']), 0, 1, "cb_check_imap_esc", vi_buffer) elif (vi_buffer and imap_esc.startswith(vi_buffer) and imap_esc[len(vi_buffer):len(vi_buffer)+1] != keys): vi_buffer = "" weechat.bar_item_update("vi_buffer") # imap_esc sequence detected -- remove the sequence keys from the # Weechat input bar and enter Normal mode. if imap_esc == vi_buffer: buf = weechat.current_buffer() input_line = weechat.buffer_get_string(buf, "input") cur = weechat.buffer_get_integer(buf, "input_pos") input_line = (input_line[:cur-len(imap_esc)+1] + input_line[cur:]) weechat.buffer_set(buf, "input", input_line) set_cur(buf, input_line, cur-len(imap_esc)+1, False) set_mode("NORMAL") vi_buffer = "" weechat.bar_item_update("vi_buffer") return weechat.WEECHAT_RC_OK_EAT return weechat.WEECHAT_RC_OK # We're in Replace mode — allow "normal" key presses (e.g. "a") and # overwrite the next character with them, but let the other key presses # pass normally (e.g. backspace, arrow keys, etc). if mode == "REPLACE": if len(keys) == 1: weechat.command("", "/input delete_next_char") elif keys == "\x01?": weechat.command("", "/input move_previous_char") return weechat.WEECHAT_RC_OK_EAT return weechat.WEECHAT_RC_OK # We're catching keys! Only "normal" key presses interest us (e.g. "a"), # not complex ones (e.g. backspace). if len(keys) == 1 and catching_keys_data['amount']: catching_keys_data['keys'] += keys catching_keys_data['amount'] -= 1 # Done catching keys, execute the callback. if catching_keys_data['amount'] == 0: globals()[catching_keys_data['callback']]() vi_buffer = "" weechat.bar_item_update("vi_buffer") return weechat.WEECHAT_RC_OK_EAT # We're in command-line mode. if mode == "COMMAND": buf = weechat.current_buffer() cmd_text = weechat.buffer_get_string(buf, "input") weechat.hook_timer(1, 0, 1, "cb_check_cmd_mode", "") # Return key. if keys == "\x01M": weechat.hook_timer(1, 0, 1, "cb_exec_cmd", cmd_text) if len(cmd_text) > 1 and (not cmd_history or cmd_history[-1] != cmd_text): cmd_history.append(cmd_text) cmd_history_index = 0 set_mode("NORMAL") buf = weechat.current_buffer() input_line = input_line_backup[buf]['input_line'] weechat.buffer_set(buf, "input", input_line) set_cur(buf, input_line, input_line_backup[buf]['cur'], False) # Up arrow. elif keys == "\x01[[A": if cmd_history_index > -len(cmd_history): cmd_history_index -= 1 cmd_text = cmd_history[cmd_history_index] weechat.buffer_set(buf, "input", cmd_text) set_cur(buf, cmd_text, len(cmd_text), False) # Down arrow. elif keys == "\x01[[B": if cmd_history_index < -1: cmd_history_index += 1 cmd_text = cmd_history[cmd_history_index] else: cmd_history_index = 0 cmd_text = ":" weechat.buffer_set(buf, "input", cmd_text) set_cur(buf, cmd_text, len(cmd_text), False) # Tab key. No completion when searching ("/"). elif keys == "\x01I" and cmd_text[0] == ":": if cmd_text_orig is None: input_ = list(cmd_text) del input_[0] cmd_text_orig = "".join(input_) cmd_compl_list = [] for cmd in VI_COMMANDS.keys(): if cmd.startswith(cmd_text_orig): cmd_compl_list.append(cmd) if cmd_compl_list: curr_suggestion = cmd_compl_list[cmd_compl_pos] cmd_text = ":%s" % curr_suggestion cmd_compl_list[cmd_compl_pos] = weechat.string_eval_expression( "${color:bold}%s${color:-bold}" % curr_suggestion, {}, {}, {}) cmd_compl_text = ", ".join(cmd_compl_list) cmd_compl_pos = (cmd_compl_pos + 1) % len(cmd_compl_list) weechat.buffer_set(buf, "input", cmd_text) set_cur(buf, cmd_text, len(cmd_text), False) # Input. else: cmd_compl_text = "" cmd_text_orig = None cmd_compl_pos = 0 weechat.bar_item_update("cmd_completion") if keys in ["\x01M", "\x01[[A", "\x01[[B"]: cmd_compl_text = "" return weechat.WEECHAT_RC_OK_EAT else: return weechat.WEECHAT_RC_OK # Enter command mode. elif keys in [":", "/"]: if keys == "/": weechat.command("", "/input search_text_here") if not weechat.config_string_to_boolean( vimode_settings['search_vim']): return weechat.WEECHAT_RC_OK else: buf = weechat.current_buffer() cur = weechat.buffer_get_integer(buf, "input_pos") input_line = weechat.buffer_get_string(buf, "input") input_line_backup[buf] = {'input_line': input_line, 'cur': cur} input_line = ":" weechat.buffer_set(buf, "input", input_line) set_cur(buf, input_line, 1, False) set_mode("COMMAND") cmd_compl_text = "" cmd_text_orig = None cmd_compl_pos = 0 return weechat.WEECHAT_RC_OK_EAT # Add key to the buffer. vi_buffer += keys weechat.bar_item_update("vi_buffer") if not vi_buffer: return weechat.WEECHAT_RC_OK # Check if the keys have a (partial or full) match. If so, also get the # keys without the count. (These are the actual keys we should handle.) # After that, `vi_buffer` is only used for display purposes — only # `vi_keys` is checked for all the handling. # If no matches are found, the keys buffer is cleared. matched, vi_keys, count = get_keys_and_count(vi_buffer) if not matched: vi_buffer = "" return weechat.WEECHAT_RC_OK_EAT # Check if it's a command (user defined key mapped to a :cmd). if vi_keys.startswith(":"): weechat.hook_timer(1, 0, 1, "cb_exec_cmd", "{} {}".format(vi_keys, count)) vi_buffer = "" return weechat.WEECHAT_RC_OK_EAT # It's a WeeChat command (user defined key mapped to a /cmd). if vi_keys.startswith("/"): weechat.command("", vi_keys) vi_buffer = "" return weechat.WEECHAT_RC_OK_EAT buf = weechat.current_buffer() input_line = weechat.buffer_get_string(buf, "input") cur = weechat.buffer_get_integer(buf, "input_pos") # It's a default mapping. If the corresponding value is a string, we assume # it's a WeeChat command. Otherwise, it's a method we'll call. if vi_keys in VI_KEYS: if vi_keys not in ['u', '\x01R']: add_undo_history(buf, input_line) if isinstance(VI_KEYS[vi_keys], str): for _ in range(max(count, 1)): # This is to avoid crashing WeeChat on script reloads/unloads, # because no hooks must still be running when a script is # reloaded or unloaded. if (VI_KEYS[vi_keys] == "/input return" and input_line.startswith("/script ")): return weechat.WEECHAT_RC_OK weechat.command("", VI_KEYS[vi_keys]) current_cur = weechat.buffer_get_integer(buf, "input_pos") set_cur(buf, input_line, current_cur) else: VI_KEYS[vi_keys](buf, input_line, cur, count) # It's a motion (e.g. "w") — call `motion_X()` where X is the motion, then # set the cursor's position to what that function returned. elif vi_keys in VI_MOTIONS: if vi_keys in SPECIAL_CHARS: func = "motion_%s" % SPECIAL_CHARS[vi_keys] else: func = "motion_%s" % vi_keys end, _, _ = globals()[func](input_line, cur, count) set_cur(buf, input_line, end) # It's an operator + motion (e.g. "dw") — call `motion_X()` (where X is # the motion), then we call `operator_Y()` (where Y is the operator) # with the position `motion_X()` returned. `operator_Y()` should then # handle changing the input line. elif (len(vi_keys) > 1 and vi_keys[0] in VI_OPERATORS and vi_keys[1:] in VI_MOTIONS): add_undo_history(buf, input_line) if vi_keys[1:] in SPECIAL_CHARS: func = "motion_%s" % SPECIAL_CHARS[vi_keys[1:]] else: func = "motion_%s" % vi_keys[1:] pos, overwrite, catching = globals()[func](input_line, cur, count) # If it's a catching motion, we don't want to call the operator just # yet -- this code will run again when the motion is complete, at which # point we will. if not catching: oper = "operator_%s" % vi_keys[0] globals()[oper](buf, input_line, cur, pos, overwrite) # The combo isn't completed yet (e.g. just "d"). else: return weechat.WEECHAT_RC_OK_EAT # We've already handled the key combo, so clear the keys buffer. if not catching_keys_data['amount']: vi_buffer = "" weechat.bar_item_update("vi_buffer") return weechat.WEECHAT_RC_OK_EAT def cb_check_imap_esc(data, remaining_calls): """Clear the imap_esc sequence after some time if nothing was pressed.""" global vi_buffer if vi_buffer == data: vi_buffer = "" weechat.bar_item_update("vi_buffer") return weechat.WEECHAT_RC_OK def cb_key_combo_search(data, signal, signal_data): """Handle keys while search mode is active (if search_vim is enabled).""" if not weechat.config_string_to_boolean(vimode_settings['search_vim']): return weechat.WEECHAT_RC_OK if mode == "COMMAND": if signal_data == "\x01M": set_mode("SEARCH") return weechat.WEECHAT_RC_OK_EAT elif mode == "SEARCH": if signal_data == "\x01M": set_mode("NORMAL") else: if signal_data == "n": weechat.command("", "/input search_next") elif signal_data == "N": weechat.command("", "/input search_previous") # Start a new search. elif signal_data == "/": weechat.command("", "/input search_stop_here") set_mode("NORMAL") weechat.command("", "/input search_text_here") return weechat.WEECHAT_RC_OK_EAT return weechat.WEECHAT_RC_OK # Callbacks. # ========== # Bar items. # ---------- def cb_vi_buffer(data, item, window): """Return the content of the vi buffer (pressed keys on hold).""" return vi_buffer def cb_cmd_completion(data, item, window): """Return the text of the command line.""" return cmd_compl_text def cb_mode_indicator(data, item, window): """Return the current mode (INSERT/NORMAL/REPLACE/...).""" return "{}{}{}{}{}".format( weechat.color(mode_colors[mode]), vimode_settings['mode_indicator_prefix'], mode, vimode_settings['mode_indicator_suffix'], weechat.color("reset")) def cb_line_numbers(data, item, window): """Fill the line numbers bar item.""" bar_height = weechat.window_get_integer(window, "win_chat_height") content = "" for i in range(1, bar_height + 1): content += "{}{}{}\n".format(vimode_settings['line_number_prefix'], i, vimode_settings['line_number_suffix']) return content # Callbacks for the line numbers bar. # ................................... def cb_update_line_numbers(data, signal, signal_data): """Call `cb_timer_update_line_numbers()` when switching buffers. A timer is required because the bar item is refreshed before the new buffer is actually displayed, so ``win_chat_height`` would refer to the old buffer. Using a timer refreshes the item after the new buffer is displayed. """ weechat.hook_timer(10, 0, 1, "cb_timer_update_line_numbers", "") return weechat.WEECHAT_RC_OK def cb_timer_update_line_numbers(data, remaining_calls): """Update the line numbers bar item.""" weechat.bar_item_update("line_numbers") return weechat.WEECHAT_RC_OK # Config. # ------- def cb_config(data, option, value): """Script option changed, update our copy.""" option_name = option.split(".")[-1] if option_name in vimode_settings: vimode_settings[option_name] = value if option_name == 'user_mappings': load_user_mappings() if "_color" in option_name: load_mode_colors() return weechat.WEECHAT_RC_OK def load_mode_colors(): mode_colors.update({ 'NORMAL': "{},{}".format( vimode_settings['mode_indicator_normal_color'], vimode_settings['mode_indicator_normal_color_bg']), 'INSERT': "{},{}".format( vimode_settings['mode_indicator_insert_color'], vimode_settings['mode_indicator_insert_color_bg']), 'REPLACE': "{},{}".format( vimode_settings['mode_indicator_replace_color'], vimode_settings['mode_indicator_replace_color_bg']), 'COMMAND': "{},{}".format( vimode_settings['mode_indicator_cmd_color'], vimode_settings['mode_indicator_cmd_color_bg']), 'SEARCH': "{},{}".format( vimode_settings['mode_indicator_search_color'], vimode_settings['mode_indicator_search_color_bg']) }) def load_user_mappings(): """Load user-defined mappings.""" mappings = {} if vimode_settings['user_mappings']: mappings.update(json.loads(vimode_settings['user_mappings'])) vimode_settings['user_mappings'] = mappings # Command-line execution. # ----------------------- def cb_exec_cmd(data, remaining_calls): """Translate and execute our custom commands to WeeChat command.""" # Process the entered command. data = list(data) del data[0] data = "".join(data) # s/foo/bar command. if data.startswith("s/"): cmd = data parsed_cmd = next(csv.reader(StringIO(cmd), delimiter="/", escapechar="\\")) pattern = re.escape(parsed_cmd[1]) repl = parsed_cmd[2] repl = re.sub(r"([^\\])&", r"\1" + pattern, repl) flag = None if len(parsed_cmd) == 4: flag = parsed_cmd[3] count = 1 if flag == "g": count = 0 buf = weechat.current_buffer() input_line = weechat.buffer_get_string(buf, "input") input_line = re.sub(pattern, repl, input_line, count) weechat.buffer_set(buf, "input", input_line) # Shell command. elif data.startswith("!"): weechat.command("", "/exec -buffer shell %s" % data[1:]) # Commands like `:22`. This should start cursor mode (``/cursor``) and take # us to the relevant line. elif data.isdigit(): line_number = int(data) hdata_window = weechat.hdata_get("window") window = weechat.current_window() x = weechat.hdata_integer(hdata_window, window, "win_chat_x") y = (weechat.hdata_integer(hdata_window, window, "win_chat_y") + (line_number - 1)) weechat.command("", "/cursor go {},{}".format(x, y)) # Check againt defined commands. elif data: raw_data = data data = data.split(" ", 1) cmd = data[0] args = "" if len(data) == 2: args = data[1] if cmd in VI_COMMANDS: if isinstance(VI_COMMANDS[cmd], str): weechat.command("", "%s %s" % (VI_COMMANDS[cmd], args)) else: VI_COMMANDS[cmd](args) else: # Check for commands not sepearated by space (e.g. "b2") for i in range(1, len(raw_data)): tmp_cmd = raw_data[:i] tmp_args = raw_data[i:] if tmp_cmd in VI_COMMANDS and tmp_args.isdigit(): weechat.command("", "%s %s" % (VI_COMMANDS[tmp_cmd], tmp_args)) return weechat.WEECHAT_RC_OK # No vi commands found, run the command as WeeChat command weechat.command("", "/{} {}".format(cmd, args)) return weechat.WEECHAT_RC_OK def cb_vimode_go_to_normal(data, buf, args): set_mode("NORMAL") return weechat.WEECHAT_RC_OK # Script commands. # ---------------- def cb_vimode_cmd(data, buf, args): """Handle script commands (``/vimode ``).""" # ``/vimode`` or ``/vimode help`` if not args or args == "help": weechat.prnt("", "[vimode.py] %s" % README_URL) # ``/vimode bind_keys`` or ``/vimode bind_keys --list`` elif args.startswith("bind_keys"): infolist = weechat.infolist_get("key", "", "default") weechat.infolist_reset_item_cursor(infolist) commands = ["/key unbind ctrl-W", "/key bind ctrl-W /input delete_previous_word", "/key bind ctrl-^ /input jump_last_buffer_displayed", "/key bind ctrl-Wh /window left", "/key bind ctrl-Wj /window down", "/key bind ctrl-Wk /window up", "/key bind ctrl-Wl /window right", "/key bind ctrl-W= /window balance", "/key bind ctrl-Wx /window swap", "/key bind ctrl-Ws /window splith", "/key bind ctrl-Wv /window splitv", "/key bind ctrl-Wq /window merge"] while weechat.infolist_next(infolist): key = weechat.infolist_string(infolist, "key") if re.match(REGEX_PROBLEMATIC_KEYBINDINGS, key): commands.append("/key unbind %s" % key) weechat.infolist_free(infolist) if args == "bind_keys": weechat.prnt("", "Running commands:") for command in commands: weechat.command("", command) weechat.prnt("", "Done.") elif args == "bind_keys --list": weechat.prnt("", "Listing commands we'll run:") for command in commands: weechat.prnt("", " %s" % command) weechat.prnt("", "Done.") return weechat.WEECHAT_RC_OK # Helpers. # ======== # Motions/keys helpers. # --------------------- def get_pos(data, regex, cur, ignore_cur=False, count=0): """Return the position of `regex` match in `data`, starting at `cur`. Args: data (str): the data to search in. regex (pattern): regex pattern to search for. cur (int): where to start the search. ignore_cur (bool, optional): should the first match be ignored if it's also the character at `cur`? Defaults to False. count (int, optional): the index of the match to return. Defaults to 0. Returns: int: position of the match. -1 if no matches are found. """ # List of the *positions* of the found patterns. matches = [m.start() for m in re.finditer(regex, data[cur:])] pos = -1 if count: if len(matches) > count - 1: if ignore_cur and matches[0] == 0: if len(matches) > count: pos = matches[count] else: pos = matches[count - 1] elif matches: if ignore_cur and matches[0] == 0: if len(matches) > 1: pos = matches[1] else: pos = matches[0] return pos def set_cur(buf, input_line, pos, cap=True): """Set the cursor's position. Args: buf (str): pointer to the current WeeChat buffer. input_line (str): the content of the input line. pos (int): the position to set the cursor to. cap (bool, optional): if True, the `pos` will shortened to the length of `input_line` if it's too long. Defaults to True. """ if cap: pos = min(pos, len(input_line) - 1) weechat.buffer_set(buf, "input_pos", str(pos)) def start_catching_keys(amount, callback, input_line, cur, count, buf=None): """Start catching keys. Used for special commands (e.g. "f", "r"). amount (int): amount of keys to catch. callback (str): name of method to call once all keys are caught. input_line (str): input line's content. cur (int): cursor's position. count (int): count, e.g. "2" for "2fs". buf (str, optional): pointer to the current WeeChat buffer. Defaults to None. `catching_keys_data` is a dict with the above arguments, as well as: keys (str): pressed keys will be added under this key. new_cur (int): the new cursor's position, set in the callback. When catching keys is active, normal pressed keys (e.g. "a" but not arrows) will get added to `catching_keys_data` under the key "keys", and will not be handled any further. Once all keys are caught, the method defined in the "callback" key is called, and can use the data in `catching_keys_data` to perform its action. """ global catching_keys_data if "new_cur" in catching_keys_data: new_cur = catching_keys_data['new_cur'] catching_keys_data = {'amount': 0} return new_cur, True, False catching_keys_data = ({'amount': amount, 'callback': callback, 'input_line': input_line, 'cur': cur, 'keys': "", 'count': count, 'new_cur': 0, 'buf': buf}) return cur, False, True def get_keys_and_count(combo): """Check if `combo` is a valid combo and extract keys/counts if so. Args: combo (str): pressed keys combo. Returns: matched (bool): True if the combo has a (partial or full) match, False otherwise. combo (str): `combo` with the count removed. These are the actual keys we should handle. User mappings are also expanded. count (int): count for `combo`. """ # Look for a potential match (e.g. "d" might become "dw" or "dd" so we # accept it, but "d9" is invalid). matched = False # Digits are allowed at the beginning (counts or "0"). count = 0 if combo.isdigit(): matched = True elif combo and combo[0].isdigit(): count = "" for char in combo: if char.isdigit(): count += char else: break combo = combo.replace(count, "", 1) count = int(count) # It's a user defined key. Expand it. if combo in vimode_settings['user_mappings']: combo = vimode_settings['user_mappings'][combo] # It's a WeeChat command. if not matched and combo.startswith("/"): matched = True # Check against defined keys. if not matched: for key in VI_KEYS: if key.startswith(combo): matched = True break # Check against defined motions. if not matched: for motion in VI_MOTIONS: if motion.startswith(combo): matched = True break # Check against defined operators + motions. if not matched: for operator in VI_OPERATORS: if combo.startswith(operator): # Check for counts before the motion (but after the operator). vi_keys_no_op = combo[len(operator):] # There's no motion yet. if vi_keys_no_op.isdigit(): matched = True break # Get the motion count, then multiply the operator count by # it, similar to vim's behavior. elif vi_keys_no_op and vi_keys_no_op[0].isdigit(): motion_count = "" for char in vi_keys_no_op: if char.isdigit(): motion_count += char else: break # Remove counts from `vi_keys_no_op`. combo = combo.replace(motion_count, "", 1) motion_count = int(motion_count) count = max(count, 1) * motion_count # Check against defined motions. for motion in VI_MOTIONS: if motion.startswith(combo[1:]): matched = True break return matched, combo, count # Other helpers. # -------------- def set_mode(arg): """Set the current mode and update the bar mode indicator.""" global mode buf = weechat.current_buffer() input_line = weechat.buffer_get_string(buf, "input") if mode == "INSERT" and arg == "NORMAL": add_undo_history(buf, input_line) mode = arg # If we're going to Normal mode, the cursor must move one character to the # left. if mode == "NORMAL": cur = weechat.buffer_get_integer(buf, "input_pos") set_cur(buf, input_line, cur - 1, False) weechat.bar_item_update("mode_indicator") def cb_check_cmd_mode(data, remaining_calls): """Exit command mode if user erases the leading ':' character.""" buf = weechat.current_buffer() cmd_text = weechat.buffer_get_string(buf, "input") if not cmd_text: set_mode("NORMAL") return weechat.WEECHAT_RC_OK def add_undo_history(buf, input_line): """Add an item to the per-buffer undo history.""" if buf in undo_history: if not undo_history[buf] or undo_history[buf][-1] != input_line: undo_history[buf].append(input_line) undo_history_index[buf] = -1 else: undo_history[buf] = ['', input_line] undo_history_index[buf] = -1 def clear_undo_history(buf): """Clear the undo history for a given buffer.""" undo_history[buf] = [''] undo_history_index[buf] = -1 def print_warning(text): """Print warning, in red, to the current buffer.""" weechat.prnt("", ("%s[vimode.py] %s" % (weechat.color("red"), text))) def check_warnings(): """Warn the user about problematic key bindings and tmux/screen.""" user_warned = False # Warn the user about problematic key bindings that may conflict with # vimode. # The solution is to remove these key bindings, but that's up to the user. infolist = weechat.infolist_get("key", "", "default") problematic_keybindings = [] while weechat.infolist_next(infolist): key = weechat.infolist_string(infolist, "key") command = weechat.infolist_string(infolist, "command") if re.match(REGEX_PROBLEMATIC_KEYBINDINGS, key): problematic_keybindings.append("%s -> %s" % (key, command)) weechat.infolist_free(infolist) if problematic_keybindings: user_warned = True print_warning("Problematic keybindings detected:") for keybinding in problematic_keybindings: print_warning(" %s" % keybinding) print_warning("These keybindings may conflict with vimode.") print_warning("You can remove problematic key bindings and add" " recommended ones by using /vimode bind_keys, or only" " list them with /vimode bind_keys --list") print_warning("For help, see: %s" % FAQ_KEYBINDINGS) del problematic_keybindings # Warn tmux/screen users about possible Esc detection delays. if "STY" in os.environ or "TMUX" in os.environ: if user_warned: weechat.prnt("", "") user_warned = True print_warning("tmux/screen users, see: %s" % FAQ_ESC) if (user_warned and not weechat.config_string_to_boolean(vimode_settings['no_warn'])): if user_warned: weechat.prnt("", "") print_warning("To force disable warnings, you can set" " plugins.var.python.vimode.no_warn to 'on'") # Main script. # ============ if __name__ == "__main__": weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "") # Warn the user if he's using an unsupported WeeChat version. VERSION = weechat.info_get("version_number", "") if int(VERSION) < 0x01000000: print_warning("Please upgrade to WeeChat ≥ 1.0.0. Previous versions" " are not supported.") # Set up script options. for option, value in list(vimode_settings.items()): if weechat.config_is_set_plugin(option): vimode_settings[option] = weechat.config_get_plugin(option) else: weechat.config_set_plugin(option, value[0]) vimode_settings[option] = value[0] weechat.config_set_desc_plugin(option, "%s (default: \"%s\")" % (value[1], value[0])) load_user_mappings() load_mode_colors() # Warn the user about possible problems if necessary. if not weechat.config_string_to_boolean(vimode_settings['no_warn']): check_warnings() # Create bar items and setup hooks. weechat.bar_item_new("mode_indicator", "cb_mode_indicator", "") weechat.bar_item_new("cmd_completion", "cb_cmd_completion", "") weechat.bar_item_new("vi_buffer", "cb_vi_buffer", "") weechat.bar_item_new("line_numbers", "cb_line_numbers", "") if int(VERSION) >= 0x02090000: weechat.bar_new("vi_line_numbers", "on", "0", "window", "", "left", "vertical", "vertical", "0", "0", "default", "default", "default", "default", "0", "line_numbers") else: weechat.bar_new("vi_line_numbers", "on", "0", "window", "", "left", "vertical", "vertical", "0", "0", "default", "default", "default", "0", "line_numbers") weechat.hook_config("plugins.var.python.%s.*" % SCRIPT_NAME, "cb_config", "") weechat.hook_signal("key_pressed", "cb_key_pressed", "") weechat.hook_signal("key_combo_default", "cb_key_combo_default", "") weechat.hook_signal("key_combo_search", "cb_key_combo_search", "") weechat.hook_signal("buffer_switch", "cb_update_line_numbers", "") weechat.hook_command("vimode", SCRIPT_DESC, "[help | bind_keys [--list]]", " help: show help\n" "bind_keys: unbind problematic keys, and bind" " recommended keys to use in WeeChat\n" " --list: only list changes", "help || bind_keys |--list", "cb_vimode_cmd", "") weechat.hook_command("vimode_go_to_normal", ("This command can be used for key bindings to go to " "normal mode."), "", "", "", "cb_vimode_go_to_normal", "") # Remove obsolete bar. vi_cmd_bar = weechat.bar_search("vi_cmd") weechat.bar_remove(vi_cmd_bar) weechat-scripts/python/keepnick.py0000644000175100017510000003530115112622402016330 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2012-2017 by nils_2 # Copyright (c) 2006 by EgS # # script to keep your nick and recover it in case it's occupied # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # 2023-06-14: Sébastien Helleu # 1.7 : remove use of infolist variable "ssl_connected" # # 2017-10-19: nils_2 (freenode.#weechat) # 1.6 : fix parsing error, now using weechat.info_get_hashtable() (reported by Mikaela) # # 2017-10-14: Kaijo & nils_2 (freenode.#weechat) # 1.5 : fix empty string breaks output # : add evaluation for option "text" and use variable "$server" instead of "%s" # # 2017-09-06: nils_2 (freenode.#weechat) # 1.4.2: fix missing weechat.config_string() # # 2017-08-17: nils_2 (freenode.#weechat) # 1.4.1: fix eval_expression for nicks # : add evaluation for options # # 2017-08-17: nils_2 (freenode.#weechat) # 1.4 : eval_expression for nicks # : use irc.server..password # : add short /help # # 2016-05-12: picasso (freenode.#weechat) # 1.3 : monitor quits and nick changes # # 2014-10-30: nils_2 (freenode.#weechat) # 1.2 : support of regular expression for server option (idea by michele) # # 2013-09-17: nils_2 (freenode.#weechat) # 1.1 : format of weechat_string_eval_expression() changed # # 2013-07-29: nils_2 (freenode.#weechat) # 1.0 : support of /secure for passwords # # 2013-01-29: nils_2 (freenode.#weechat) # 0.9 : script optimized # # 2013-01-27: nils_2 (freenode.#weechat) # 0.8 : make script compatible with Python 3.x # # 2012-10-05: nils_2, (freenode.#weechat) # 0.7 : add options "plugins.var.python.keepnick.nickserv", "plugins.var.python.keepnick..password" # : changed default "delay" value from 60 to 600 seconds. # # 2012-10-04: nils_2, (freenode.#weechat) # 0.6 : fix bug with case-sensitive nicks (reported by Faethor) # # 2012-02-08: nils_2, (freenode.#weechat) # 0.5 : sync with 0.3.x API (requested by CAHbI4) # # requires: WeeChat version 1.3 # # Development is currently hosted at # https://github.com/weechatter/weechat-scripts # ####################################################################### # # # This script enables the user to keep their nicks and recover it in # # case it get's stolen. It uses the servers prefered nicks, so there # # is no need for any kind of setup. # # # # Name: Keepnick # # Licence: GPL v2 # # Author: Marcus Eggenberger # # # Changelog: # # 0.4: now starts on load and features user defined check intervals # # 0.3: Fixed major bug with continuous nickchanges # # 0.2: Fixed Bug: now only checks connected servers # # 0.1: first version released # # # ####################################################################### try: from string import Template import weechat,sys,re except Exception: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") quit() # -------------------------------[ Constants ]------------------------------------- SCRIPT_NAME = "keepnick" SCRIPT_AUTHOR = "nils_2 " SCRIPT_VERSION = "1.7" SCRIPT_LICENCE = "GPL3" SCRIPT_DESC = "keep your nick and recover it in case it's occupied" ISON = '/ison %s' OPTIONS = { 'delay' : ('600','delay (in seconds) to look at occupied nick (0 means OFF). It is not recommended to flood the server with /ison requests)'), 'timeout' : ('60','timeout (in seconds) to wait for an answer from server.'), 'serverlist' : ('','comma separated list of servers to look at. Try to register a nickname on server (see: /msg NickServ help).regular expression are allowed (eg. ".*" = matches ALL server,"freen.*" = matches freenode, freenet....) (this option is evaluated).'), 'text' : ('Nickstealer left Network: $server!','text to display, when you get your nick back. (\"$server and $nick\" can be used) (this option is evaluated).'), 'nickserv' : ('/msg -server $server NICKSERV IDENTIFY $passwd','Use SASL authentification, if possible. This command will be used to IDENTIFY you on server (following placeholder can be used: \"$server\" for servername; \"$passwd\" for password). You can create an option for every server to store password: \"plugins.var.python.%s..password\", otherwise the \"irc.server..password\" option will be used (this option is evaluated).' % SCRIPT_NAME), 'command' : ('/nick %s','This command will be used to rename your nick (\"%s\" will be replaced with your nickname)'), 'debug' : ('off', 'When enabled, will output verbose debugging information during script operation'), } HOOK = { 'timer': '', 'redirect': '', 'quit': '', 'nick': '' } # ================================[ redirection ]=============================== # calling /ison all x seconds using hook:timer() def ison(servername,nick,nicklist): command = ISON % ' '.join(nicklist) debug_print("Checking nicks on server %s with command: %s" % (servername, command) ) weechat.hook_hsignal_send('irc_redirect_command', { 'server': servername, 'pattern': 'ison', 'signal': SCRIPT_NAME, 'count': '1', 'string': servername, 'timeout': OPTIONS['timeout'], 'cmd_filter': '' }) weechat.hook_signal_send('irc_input_send', weechat.WEECHAT_HOOK_SIGNAL_STRING, '%s;;;;%s' % (servername,command)) def redirect_isonhandler(data, signal, hashtable): debug_print("ISON response: %s" % hashtable['output']) if hashtable['output'] == '': return weechat.WEECHAT_RC_OK parsed = weechat.info_get_hashtable( "irc_message_parse",dict(message=hashtable['output']) ) # variable ISON_nicks contains online nicks on server (separated with space) # nicks in variable ISON_nicks are lowercase and 'text' contains the nick ISON_nicks = [ nick.lower() for nick in parsed['text'].split() ] for nick in server_nicks(hashtable['server']): mynick = weechat.info_get('irc_nick',hashtable['server']) # current nick on server if nick.lower() == mynick.lower(): debug_print("I already have nick %s; not changing" % mynick) return weechat.WEECHAT_RC_OK elif nick.lower() not in ISON_nicks and nick != '': grabnick_and_auth(hashtable['server'], nick) return weechat.WEECHAT_RC_OK return weechat.WEECHAT_RC_OK # ================================[ functions ]=============================== # nicks used on server def server_nicks(servername): infolist = weechat.infolist_get('irc_server','',servername) weechat.infolist_next(infolist) nicks = string_eval_expression( weechat.infolist_string(infolist, 'nicks') ) # nicks in config weechat.infolist_free(infolist) return nicks.split(',') def server_enabled(servername): serverlist = string_eval_expression( OPTIONS['serverlist'] ).split(',') server_matched = re.search(r"\b({})\b".format("|".join(serverlist)),servername) if servername in serverlist or server_matched: return True else: return False def check_nicks(data, remaining_calls): infolist = weechat.infolist_get('irc_server','','') while weechat.infolist_next(infolist): servername = weechat.infolist_string(infolist, 'name') ptr_buffer = weechat.infolist_pointer(infolist,'buffer') nick = weechat.infolist_string(infolist, 'nick') is_connected = weechat.infolist_integer(infolist,'is_connected') if server_enabled(servername): if nick and is_connected: ison(servername,nick,server_nicks(servername)) weechat.infolist_free(infolist) return weechat.WEECHAT_RC_OK def check_quit(data, signal, signal_data): servername = signal.split(',')[0] hostmask = signal_data.split(':')[1].split(' ')[0] nick = hostmask.split('!')[0] nicks = server_nicks(servername) if server_enabled(servername) and nick in nicks: debug_print("Saw %s quit on %s; checking nick" % (nick, servername)) ison(servername, nick, nicks) return weechat.WEECHAT_RC_OK def check_nick_change(data, signal, signal_data): servername = signal.split(',')[0] hostmask = signal_data.split(':')[1].split(' ')[0] nick = hostmask.split('!')[0] nicks = server_nicks(servername) if server_enabled(servername) and nick in nicks: my_nick = my_nick_on_server(servername) if my_nick.lower() != nick.lower(): debug_print("Saw %s change nick on %s; checking nick" % (nick, servername)) ison(servername, nick, nicks) else: debug_print("Saw my own nick change on server (%s); ignoring" % nick) return weechat.WEECHAT_RC_OK def my_nick_on_server(servername): nick = weechat.info_get("irc_nick", servername) return nick def grabnick_and_auth(servername, nick): global OPTIONS password = string_eval_expression( weechat.config_get_plugin('%s.password' % servername) ) if not password: password = string_eval_expression( weechat.config_string(weechat.config_get("irc.server.%s.password" % servername)) ) grabnick(servername, nick) # get your nick back if password != '' and OPTIONS['nickserv'] != '': # command stored in "keepnick.nickserv" option t = Template(string_eval_expression( OPTIONS['nickserv']) ) run_msg = t.safe_substitute(server=servername, passwd=password) weechat.command('', run_msg) def string_eval_expression(string): return weechat.string_eval_expression(string,{},{},{}) def grabnick(servername, nick): if nick and servername: if OPTIONS['text']: t = Template( string_eval_expression(OPTIONS['text']) ) text = t.safe_substitute(server=servername, nick=nick) weechat.prnt(weechat.current_buffer(), text) weechat.command(weechat.buffer_search('irc','%s.%s' % ('server',servername)), OPTIONS['command'] % nick) # ================================[ weechat hook ]=============================== def install_hooks(): global HOOK,OPTIONS # Should never happen if any([HOOK[k] != '' for k in HOOK]): return if not OPTIONS['delay'] or not OPTIONS['timeout']: return debug_print("Installing timer with delay %s seconds" % OPTIONS['delay']) HOOK['timer'] = weechat.hook_timer(int(OPTIONS['delay']) * 1000, 0, 0, 'check_nicks', '') HOOK['redirect'] = weechat.hook_hsignal('irc_redirection_%s_ison' % SCRIPT_NAME, 'redirect_isonhandler', '' ) HOOK['quit'] = weechat.hook_signal('*,irc_raw_in_quit', 'check_quit', '') HOOK['nick'] = weechat.hook_signal('*,irc_raw_in_nick', 'check_nick_change', '') for k in HOOK: if HOOK[k] == 0: weechat.prnt('', "%s: can't enable %s, hook_timer() failed" % (weechat.prefix('error'), SCRIPT_NAME)) def remove_hooks(): global HOOK for k in HOOK: if HOOK[k] != '': weechat.unhook(HOOK[k]) HOOK[k] = '' def debug_print(msg): global OPTIONS if OPTIONS['debug'] != 'off': weechat.prnt('', "%s DEBUG: %s" % (SCRIPT_NAME, msg)) # ================================[ weechat options and description ]=============================== def init_options(): global HOOK,OPTIONS for option,value in list(OPTIONS.items()): weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) OPTIONS[option] = value[0] else: OPTIONS[option] = weechat.config_get_plugin(option) def toggle_refresh(pointer, name, value): global HOOK,OPTIONS option = name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname OPTIONS[option] = value # save new value if option == 'delay' or option == 'timeout': if int(OPTIONS['delay']) > 0 or int(OPTIONS['timeout']) > 0: remove_hooks() install_hooks() else: remove_hooks() # user switched timer off return weechat.WEECHAT_RC_OK def print_usage(data, buffer, args): weechat.prnt(buffer, "%s\t%s: script already running..." % ( string_eval_expression(weechat.config_string(weechat.config_get("weechat.look.prefix_error"))), SCRIPT_NAME) ) return weechat.WEECHAT_RC_OK # ================================[ main ]=============================== if __name__ == '__main__': if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC, '',''): weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, '', 'You have to edit options with: /set *keepnick*\n' 'I suggest using /iset script or /fset plugin.\n', '', 'print_usage', '') version = weechat.info_get("version_number", "") or 0 if int(version) >= 0x01030000: if int(OPTIONS['delay'][0]) > 0 and int(OPTIONS['timeout'][0]) > 0: init_options() install_hooks() weechat.hook_config( 'plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) else: weechat.prnt('','%s%s %s' % (weechat.prefix('error'),SCRIPT_NAME,': needs version 1.3 or higher')) weechat.command('','/wait 1ms /python unload %s' % SCRIPT_NAME) weechat-scripts/python/confversion.py0000644000175100017510000001074715112622400017077 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2010-2010 by drubin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Allows you to visually see if there are updates to your weechat system #Versions # 0.1 drubin - First release. # - Basic functionality to save version history of your config files (only git, bzr) # 0.2 ShockkPony - Fixed massive weechat startup time caused by initial config loading # 0.3 noctux - Adapt to python 3 # 0.4 FlashCode - Add compatibility with WeeChat >= 3.2 (XDG directories) SCRIPT_NAME = "confversion" SCRIPT_AUTHOR = "drubin " SCRIPT_VERSION = "0.4" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Stores version controlled history of your configuration files" import_ok = True import subprocess try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False # script options settings = { #Currently supports git and bzr and possibly other that support simple "init" "add *.conf" "commit -m "message" " "versioning_method" : "git", "commit_each_change" : "true", "commit_message" : "Commiting changes", #Allows you to not auto commit stuff that relates to these configs #, (comma) seperated list of config options #The toggle_nicklist script can make this property annoying. "auto_commit_ignore" : "weechat.bar.nicklist.hidden", } def shell_in_home(cmd): try: output = open("/dev/null","w") subprocess.Popen(ver_method()+" "+cmd, cwd = weechat_home(), stdout= output, stderr=output, shell=True) except Exception as e: print(e) def weechat_home(): return weechat.info_get("weechat_config_dir", "") \ or weechat.info_get("weechat_dir", "") def ver_method(): return weechat.config_get_plugin("versioning_method") def init_repo(): #Set up version control (doesn't matter if previously setup for bzr, git) shell_in_home("init") #Save first import OR on start up if needed. commit_cb() confversion_commit_finish_hook = 0 def commit_cb(data=None, remaning=None): global confversion_commit_finish_hook # only hook timer if not already hooked if confversion_commit_finish_hook == 0: confversion_commit_finish_hook = weechat.hook_timer(500, 0, 1, "commit_cb_finish", "") return weechat.WEECHAT_RC_OK def commit_cb_finish(data=None, remaining=None): global confversion_commit_finish_hook # save before doing commit weechat.command("","/save") # add all config changes to git shell_in_home("add ./*.conf") # do the commit shell_in_home("commit -m \"%s\"" % weechat.config_get_plugin("commit_message")) # set hook back to 0 confversion_commit_finish_hook = 0 return weechat.WEECHAT_RC_OK def conf_update_cb(data, option, value): #Commit data if not part of ignore list. if weechat.config_get_plugin("commit_each_change") == "true" and not option in weechat.config_get_plugin("auto_commit_ignore").split(","): #Call use pause else /save will be called before the config is actually saved to disc #This is kinda hack but better input would be appricated. weechat.hook_timer(500, 0, 1, "commit_cb", "") return weechat.WEECHAT_RC_OK def confversion_cmd(data, buffer, args): commit_cb() return weechat.WEECHAT_RC_OK if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): for option, default_value in settings.items(): if weechat.config_get_plugin(option) == "": weechat.config_set_plugin(option, default_value) weechat.hook_command("confversion", "Saves configurations to version control", "", "", "", "confversion_cmd", "") init_repo() hook = weechat.hook_config("*", "conf_update_cb", "") weechat-scripts/python/queryman.py0000644000175100017510000003205415112622403016403 0ustar manumanu# # SPDX-FileCopyrightText: 2013-2025 Nils Görs # SPDX-FileCopyrightText: 2017 Filip H.F. 'FiXato' Slagter # # SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Save and restore query buffers after /quit. # # Idea by lasers@freenode.#weechat # # 2025-10-02: nils_2 (libera.#weechat) # 0.7 : fix a ValueError in config file, when using dcc. localvar "server", is missing for dcc buffer (reported by roughnecks) # : add some more DEBUG() text # # 2023-08-01: nils_2 (libera.#weechat) # 0.6.1 : fix a timing problem when joining autojoin-channels (reported by thecdnhermit) # # 2021-05-05: Sébastien Helleu # 0.6 : add compatibility with XDG directories (WeeChat >= 3.2) # # 2018-08-08: nils_2, (freenode.#weechat) # 0.5 : fix TypeError with python3.6 # # 2017-04-14: nils_2 & FiXato, (freenode.#weechat) # 0.4 : big rewrite: # : added extra hooks: # - query buffers are now also stored when opening/closing queries # - queries only restored on connect; no longer on every reconnect # : current buffer position is retained # : manual saving of query list (https://github.com/weechat/scripts/issues/196) # # 2015-02-27: nils_2, (freenode.#weechat) # 0.3 : make script consistent with "buffer_switch_autojoin" option (idea haasn) # # 2013-11-07: nils_2, (freenode.#weechat) # 0.2 : fix file not found error (reported by calcifea) # : make script compatible with Python 3.x # # 2013-07-26: nils_2, (freenode.#weechat) # 0.1 : initial release # # script will create a config file (~./weechat/queryman.txt) # format: "servername nickname" (without "") # # Development is currently hosted at # https://github.com/weechatter/weechat-scripts try: import weechat,re,os except Exception: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") quit() SCRIPT_NAME = 'queryman' SCRIPT_AUTHOR = 'nils_2 ' SCRIPT_VERSION = '0.7' SCRIPT_LICENSE = 'GPL' SCRIPT_DESC = 'save and restore query buffers after /quit and on open/close of queries' DEBUG = False queryman_filename = 'queryman.txt' servers_opening = set([]) servers_closing = set([]) stored_query_buffers_per_server = {} # ================================[ callback ]=============================== # signal_data = buffer pointer def buffer_closing_signal_cb(data, signal, signal_data): global servers_closing buf_type = weechat.buffer_get_string(signal_data,'localvar_type') # When closing a server buffer, save all buffers if buf_type == 'server': # Prevent closing private buffers on this server, from triggering saving. servers_closing.add(weechat.buffer_get_string(signal_data, 'localvar_server')) # # FIXME: This shouldn't be necessary, as buffers are already save when opened/closed # reset_stored_query_buffers() # save_stored_query_buffers_to_file() # Only save the query buffers elif buf_type == 'private': server_name = weechat.buffer_get_string(signal_data, 'localvar_server') # Don't trigger when all buffer's closing because its server buffer is closing if server_name not in servers_closing: channel_name = weechat.buffer_get_string(signal_data, 'localvar_channel') remove_channel_from_stored_list(server_name, channel_name) save_stored_query_buffers_to_file() return weechat.WEECHAT_RC_OK def quit_signal_cb(data, signal, signal_data): reset_stored_query_buffers() save_stored_query_buffers_to_file() return weechat.WEECHAT_RC_OK # signal_data = buffer pointer def irc_pv_opened_cb(data, signal, signal_data): server_name = weechat.buffer_get_string(signal_data, 'localvar_server') channel_name = weechat.buffer_get_string(signal_data, 'localvar_channel') add_channel_to_stored_list(server_name, channel_name) debug_print("signal: irc_pv_opened server: %s query: %s" % (server_name, channel_name)) save_stored_query_buffers_to_file() return weechat.WEECHAT_RC_OK # signal_data = server name def remove_server_from_servers_closing_cb(data, signal, signal_data): global servers_closing if signal_data in servers_closing: servers_closing.remove(signal_data) debug_print("signal: irc_server_closing") return weechat.WEECHAT_RC_OK # signal_data = buffer pointer def irc_server_opened_cb(data, signal, signal_data): global servers_opening server_name = weechat.buffer_get_string(signal_data, 'localvar_server') servers_opening.add(server_name) debug_print("signal: irc_server_opened %s" % server_name) return weechat.WEECHAT_RC_OK # signal_data = servername def irc_server_connected_signal_cb(data, signal, signal_data): global servers_opening # Only reopen the query buffers if the server buffer was recently opened if signal_data in servers_opening: open_stored_query_buffers_for_server(signal_data) servers_opening.remove(signal_data) debug_print("signal: irc_server_connected") return weechat.WEECHAT_RC_OK # ================================[ file ]=============================== def get_filename_with_path(): global queryman_filename path = weechat.info_get("weechat_data_dir", "") \ or weechat.info_get("weechat_dir", "") return os.path.join(path,queryman_filename) # ======== [ Stored Query Buffers List ] ========== def get_stored_list_of_query_buffers(): global stored_query_buffers_per_server filename = get_filename_with_path() stored_query_buffers_per_server = {} if os.path.isfile(filename): f = open(filename, 'r') for line in f: parts = line.strip().split() if len(parts) == 2: server_name, nick = parts stored_query_buffers_per_server.setdefault(server_name, set([])) stored_query_buffers_per_server[server_name].add(nick) f.close() else: debug_print('Error loading query buffer from "%s"' % filename) return stored_query_buffers_per_server def remove_channel_from_stored_list(server_name, channel_name): global stored_query_buffers_per_server if server_name in stored_query_buffers_per_server and channel_name in stored_query_buffers_per_server[server_name]: stored_query_buffers_per_server[server_name].remove(channel_name) if not len(stored_query_buffers_per_server[server_name]): stored_query_buffers_per_server.pop(server_name, None) def add_channel_to_stored_list(server_name, channel_name): global stored_query_buffers_per_server if server_name not in stored_query_buffers_per_server: stored_query_buffers_per_server[server_name] = set([]) if channel_name not in stored_query_buffers_per_server[server_name]: stored_query_buffers_per_server[server_name].add(channel_name) def open_query_buffer(server_name, nick): starting_buffer = weechat.current_buffer() noswitch = "" switch_autojoin = weechat.config_get("irc.look.buffer_switch_autojoin") if not weechat.config_boolean(switch_autojoin): noswitch = "-noswitch" debug_print("opening query buffer: %s on server %s" % (nick, server_name)) weechat.command('','/wait 1 /query %s -server %s %s' % ( noswitch, server_name, nick )) weechat.buffer_set(starting_buffer, 'display', 'auto') def open_stored_query_buffers_for_server(server_connected): global stored_query_buffers_per_server if server_connected in stored_query_buffers_per_server: for nick in stored_query_buffers_per_server[server_connected].copy(): debug_print("going to open query buffer: %s on connected server %s" % (nick, server_connected)) open_query_buffer(server_connected, nick) def get_current_query_buffers(): stored_query_buffers_per_server = {} ptr_infolist_buffer = weechat.infolist_get('buffer', '', '') while weechat.infolist_next(ptr_infolist_buffer): ptr_buffer = weechat.infolist_pointer(ptr_infolist_buffer,'pointer') buf_type = weechat.buffer_get_string(ptr_buffer, 'localvar_type') if buf_type == 'private': server_name = weechat.buffer_get_string(ptr_buffer, 'localvar_server') channel_name = weechat.buffer_get_string(ptr_buffer, 'localvar_channel') stored_query_buffers_per_server.setdefault(server_name, set([])) stored_query_buffers_per_server[server_name].add(channel_name) weechat.infolist_free(ptr_infolist_buffer) return stored_query_buffers_per_server def reset_stored_query_buffers(): global stored_query_buffers_per_server stored_query_buffers_per_server = get_current_query_buffers() def remove_data_file(): filename = get_filename_with_path() if os.path.isfile(filename): os.remove(filename) def save_stored_query_buffers_to_file(): global stored_query_buffers_per_server filename = get_filename_with_path() if len(stored_query_buffers_per_server): debug_print("Storing %s servers:" % len(stored_query_buffers_per_server)) try: f = open(filename, 'w') for (server_name, channels) in stored_query_buffers_per_server.items(): debug_print("Storing %s channels in server %s" % (len(channels), server_name)) for channel_name in channels: line = "%s %s" % (server_name,channel_name) debug_print(' - %s' % line) f.write("%s\n" % line) f.close() except: print_error('Error writing query buffer to "%s"' % filename) raise else: # no query buffer(s). remove file debug_print("No stored query buffers; removing data file") remove_data_file() return def print_error(message): weechat.prnt('','%s%s: %s' % (weechat.prefix('error'), SCRIPT_NAME, message)) def debug_print(message): if not DEBUG: return weechat.prnt('','DEBUG/%s: %s' % (SCRIPT_NAME, message)) def hook_command_cb(data, buffer, args): if args == "": # no args given. quit return weechat.WEECHAT_RC_OK argv = args.strip().split(" ") if argv[0].lower() == 'save': save_stored_query_buffers_to_file() return weechat.WEECHAT_RC_OK # ================================[ main ]=============================== if __name__ == '__main__': if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): version = weechat.info_get('version_number', '') or 0 if int(version) >= 0x00030700: weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, 'save', 'save : manual saving of the query list\n', '', 'hook_command_cb', '') stored_query_buffers_per_server = get_stored_list_of_query_buffers() for (server_name, channels) in get_current_query_buffers().items(): # Reopen the buffers for the channels in the servers we already have open: open_stored_query_buffers_for_server(server_name) stored_query_buffers_per_server.setdefault(server_name, set([])) debug_print("Already have %s queries for server %s: %s" % (len(stored_query_buffers_per_server[server_name]), server_name, ','.join(stored_query_buffers_per_server[server_name]))) debug_print("Adding: %s" % channels) stored_query_buffers_per_server[server_name].update(channels) debug_print("Now have %s queries for server %s: %s" % (len(stored_query_buffers_per_server[server_name]), server_name, ','.join(stored_query_buffers_per_server[server_name]))) save_stored_query_buffers_to_file() weechat.hook_signal('quit', 'quit_signal_cb', '') # weechat.hook_signal('relay_client_disconnected', 'quit_signal_cb', '') # weechat.hook_signal('relay_client_connected', 'irc_server_connected_signal_cb', '') weechat.hook_signal('irc_server_opened', 'irc_server_opened_cb', '') weechat.hook_signal('irc_server_connected', 'irc_server_connected_signal_cb','') weechat.hook_signal('irc_server_disconnected', 'remove_server_from_servers_closing_cb', '') # TODO: make these triggers optional? weechat.hook_signal('irc_pv_opened', 'irc_pv_opened_cb', '') weechat.hook_signal('buffer_closing', 'buffer_closing_signal_cb', '') weechat-scripts/python/urlgrab.py0000644000175100017510000006701115112622376016212 0ustar manumanu# -*- coding: utf-8 -*- # # UrlGrab, for weechat version >= 0.3.0 # # Listens to all channels for URLs, collects them in a list, and launches # them in your favourite web server on the local host or a remote server. # Copies url to X11 clipboard via xsel # (http://www.vergenet.net/~conrad/software/xsel) # # Usage: # # The /url command provides access to all UrlGrab functions. Run # '/help url' for complete command usage. # # In general, use '/url list' to list the entire url list for the current # channel, and '/url ' to launch the nth url in the list. For # example, to launch the first (and most-recently added) url in the list, # you would run '/url 1' # # From the server window, you must specify a specific channel for the # list and launch commands, for example: # /url list weechat # /url 3 weechat # # Configuration: # # The '/url set' command lets you get and set the following options: # # historysize # The maximum number of URLs saved per channel. Default is 10 # # method # Must be one of 'local' or 'remote' - Defines how URLs are launched by # the script. If 'local', the script will run 'localcmd' on the host. # If 'remote', the script will run 'remotessh remotehost remotecmd' on # the local host which should normally use ssh to connect to another # host and run the browser command there. # # localcmd # The command to run on the local host to launch URLs in 'local' mode. # The string '%s' will be replaced with the URL. The default is # 'xdg-open %s'. # # remotessh # The command (and arguments) used to connect to the remote host for # 'remote' mode. The default is 'ssh -x' which will connect as the # current username via ssh and disable X11 forwarding. # # remotehost # The remote host to which we will connect in 'remote' mode. For ssh, # this can just be a hostname or 'user@host' to specify a username # other than your current login name. The default is 'localhost'. # # remotecmd # The command to execute on the remote host for 'remote' mode. The # default is 'bash -c "DISPLAY=:0.0 xdg-open '%s'"' Which runs bash, sets # up the environment to display on the remote host's main X display, # and runs the default browser. As with 'localcmd', the string '%s' will be # replaced with the URL. # # cmdoutput # The file where the command output (if any) is saved. Overwritten # each time you launch a new URL. Default is ~/.weechat/urllaunch.log # # use_full_name # Whether or not to use the buffer's full name to store the URL. This can # help if you have such issues as URLs getting saved under buffers that # have a typing indicator active, or you have the same channel name in # two different networks. # # default # The command that will be run if no arguemnts to /url are given. # Default is show # # Requirements: # # - Designed to run with weechat version 0.3 or better. # https://weechat.org/ # # Acknowlegements: # # - Based on an earlier version called 'urlcollector.py' by 'kolter' of # irc.freenode.net/#weechat Honestly, I just cleaned up the code a bit and # made the settings a little more useful (to me). # # - With changes by Leonid Evdokimov (weechat at darkk dot net another dot ru): # http://darkk.net.ru/weechat/urlgrab.py # v1.1: added better handling of dead zombie-childs # added parsing of private messages # added default command setting # added parsing of scrollback buffers on load # v1.2: `historysize` was ignored # # - With changes by ExclusivE (exclusive_tm at mail dot ru): # v1.3: X11 clipboard support # # - V1.4 Just ported it over to weechat 0.2.7 drubin AT smartcube dot co dot za # - V1.5 1) I created a logging feature for urls, Time, Date, buffer, and url. # 2) Added selectable urls support, similar to the iset plugin (Thanks FlashCode) # 3) Colors/formats are configuarable. # 4) browser now uses hook_process (Please test with remote clients) # 5) Added /url open http://url.com functionality # 6) Changed urls detection to use regexpressions so should be much better # Thanks to xt of #weechat bassed on on urlbar.py # - V1.6 FlashCode : Increase timeout for hook_process # (from 1 second to 1 minute) # - V1.7 FlashCode : Update WeeChat site # - V1.8 drubin : # - Changed remote cmd to be single option # - Added scrolling on up and down arrow keys for /url show # - Changed remotecmd to include options with public/private keys password auth doesn't work # - V1.9 Specimen : # - Changed the default command when /url is run with no arguments to 'show' # - Removed '/url help' command, because /help is the standard way # - V2.0 Xilov: replace "/url help" by "/help url" # - V2.1 nand: Changed default: firefox %s to firefox '%s' (localcmd) # - V2.2 Sébastien Helleu : fix reload of config file # - V2.3 nand: Allowed trailing )s for unmatched (s in URLs # - V2.4 nand: Escaped URLs via URL-encoding instead of shell escaping, fixes ' # - V2.5 nand: Fixed some URLs that got incorrectly mangled by escaping # - V2.6 nesthib: Fixed escaping of "=" # Added missing quotes in default parameter (firefox '%s') # Removed the mix of tabs and spaces in the file indentation # - V2.7 dobbymoodge # ( https://github.com/dobbymoodge/ ): # - Added 'copycmd' setting, users can set command to pipe into # for '/url copy' # - V2.8 Simmo Saan : # - Changed print hook to ignore filtered lines # - V2.9 Dominik Heidler : # - Updated script for python3 support (now python2 and 3 are both supported) # - V3.0 Sébastien Helleu : # - Fix python 3 compatibility (replace "has_key" by "in") # - V3.1 Ron Alleva : # - Add 'use_full_name' setting, to allow storing URLs by full name of buffer # - V3.2 Marco Trevisan : # - Use xdg-open as default 'localcmd' # # Copyright (C) 2005 David Rubin # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. # from __future__ import print_function import sys import os try: import weechat import_ok = True except: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org") import_ok = False import subprocess import time try: from urllib import quote except ImportError: from urllib.parse import quote import re try: from UserDict import UserDict except ImportError: from collections import UserDict octet = r'(?:2(?:[0-4]\d|5[0-5])|1\d\d|\d{1,2})' ipAddr = r'%s(?:\.%s){3}' % (octet, octet) # Base domain regex off RFC 1034 and 1738 label = r'[0-9a-z][-0-9a-z]*[0-9a-z]?' domain = r'%s(?:\.%s)*\.[a-z][-0-9a-z]*[a-z]?' % (label, label) urlRe = re.compile(r'(\w+://(?:%s|%s)(?::\d+)?(?:/[^\]>\s]*)?)' % (domain, ipAddr), re.I) SCRIPT_NAME = "urlgrab" SCRIPT_AUTHOR = "David Rubin " SCRIPT_VERSION = "3.2" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "Url functionality Loggin, opening of browser, selectable links" CONFIG_FILE_NAME= "urlgrab" SCRIPT_COMMAND = "url" def stripParens(url): return dropChar(')', url.count(')') - url.count('('), url[::-1])[::-1] def dropChar(c, n, xs): if n == 0 or xs == []: return xs elif xs[0] == c: return dropChar(c, n-1, xs[1:]) else: return xs def urlGrabPrint(message): bufferd=weechat.current_buffer() if urlGrabSettings['output_main_buffer'] == 1 : weechat.prnt("","[%s] %s" % ( SCRIPT_NAME, message ) ) else : weechat.prnt(bufferd,"[%s] %s" % ( SCRIPT_NAME, message ) ) def hashBufferName(bufferp): if(urlGrabSettings['use_full_name']): bufferd = weechat.buffer_get_string(bufferp, "full_name") elif not weechat.buffer_get_string(bufferp, "short_name"): bufferd = weechat.buffer_get_string(bufferp, "name") else: bufferd = weechat.buffer_get_string(bufferp, "short_name") return bufferd def ug_config_reload_cb(data, config_file): """ Reload configuration file. """ return weechat.config_reload(config_file) class UrlGrabSettings(UserDict): def __init__(self): UserDict.__init__(self) self.data = {} self.config_file = weechat.config_new(CONFIG_FILE_NAME, "ug_config_reload_cb", "") if not self.config_file: return section_color = weechat.config_new_section( self.config_file, "color", 0, 0, "", "", "", "", "", "", "", "", "", "") section_default = weechat.config_new_section( self.config_file, "default", 0, 0, "", "", "", "", "", "", "", "", "", "") self.data['color_buffer']=weechat.config_new_option( self.config_file, section_color, "color_buffer", "color", "Color to display buffer name", "", 0, 0, "red", "red", 0, "", "", "", "", "", "") self.data['color_url']=weechat.config_new_option( self.config_file, section_color, "color_url", "color", "Color to display urls", "", 0, 0, "blue", "blue", 0, "", "", "", "", "", "") self.data['color_time']=weechat.config_new_option( self.config_file, section_color, "color_time", "color", "Color to display time", "", 0, 0, "cyan", "cyan", 0, "", "", "", "", "", "") self.data['color_buffer_selected']=weechat.config_new_option( self.config_file, section_color, "color_buffer_selected", "color", "Color to display buffer selected name", "", 0, 0, "red", "red", 0, "", "", "", "", "", "") self.data['color_url_selected']=weechat.config_new_option( self.config_file, section_color, "color_url_selected", "color", "Color to display url selected", "", 0, 0, "blue", "blue", 0, "", "", "", "", "", "") self.data['color_time_selected']=weechat.config_new_option( self.config_file, section_color, "color_time_selected", "color", "Color to display tim selected", "", 0, 0, "cyan", "cyan", 0, "", "", "", "", "", "") self.data['color_bg_selected']=weechat.config_new_option( self.config_file, section_color, "color_bg_selected", "color", "Background for selected row", "", 0, 0, "green", "green", 0, "", "", "", "", "", "") self.data['historysize']=weechat.config_new_option( self.config_file, section_default, "historysize", "integer", "Max number of urls to store per buffer", "", 0, 999, "10", "10", 0, "", "", "", "", "", "") self.data['method']=weechat.config_new_option( self.config_file, section_default, "method", "string", """Where to launch URLs If 'local', runs %localcmd%. If 'remote' runs the following command: '%remodecmd%'""", "", 0, 0, "local", "local", 0, "", "", "", "", "", "") self.data['copycmd']=weechat.config_new_option( self.config_file, section_default, "copycmd", "string", "Command to pipe into for 'url copy'. " "E.g. to copy into the CLIPBOARD buffer " "instead of PRIMARY, you can use 'xsel -b " "-i' here.", "", 0, 0, "xsel -i", "xsel -i", 0, "", "", "", "", "", "") self.data['localcmd']=weechat.config_new_option( self.config_file, section_default, "localcmd", "string", """Local command to execute""", "", 0, 0, "xdg-open '%s'", "xdg-open '%s'", 0, "", "", "", "", "", "") remotecmd="ssh -x localhost -i ~/.ssh/id_rsa -C \"export DISPLAY=\":0.0\" && xdg-open '%s'\"" self.data['remotecmd']=weechat.config_new_option( self.config_file, section_default, "remotecmd", "string", remotecmd, "", 0, 0, remotecmd, remotecmd, 0, "", "", "", "", "", "") self.data['use_full_name']=weechat.config_new_option( self.config_file, section_default, "use_full_name", "boolean", """Use full name of buffer to store URL""", "", 0, 0, "0", "0", 0, "", "", "", "", "", "" ) self.data['url_log']=weechat.config_new_option( self.config_file, section_default, "url_log", "string", """log location""", "", 0, 0, "~/.weechat/urls.log", "~/.weechat/urls.log", 0, "", "", "", "", "", "") self.data['time_format']=weechat.config_new_option( self.config_file, section_default, "time_format", "string", """TIme format""", "", 0, 0, "%H:%M:%S", "%H:%M:%S", 0, "", "", "", "", "", "") self.data['output_main_buffer']=weechat.config_new_option( self.config_file, section_default, "output_main_buffer", "boolean", """Print text to main buffer or current one""", "", 0, 0, "1", "1", 0, "", "", "", "", "", "") weechat.config_read(self.config_file) def __getitem__(self, key): if key == "historysize": return weechat.config_integer(self.data[key]) elif key == 'output_main_buffer': return weechat.config_boolean(self.data[key]) elif key == 'use_full_name': return weechat.config_boolean(self.data[key]) #elif key.startswith('color'): # return weechat.config_color(self.data[key]) else: return weechat.config_string(self.data[key]) def prnt(self, name, verbose = True): weechat.prnt( ""," %s = %s" % (name.ljust(11), self.data[name]) ) def prntall(self): for key in self.names(): self.prnt(key, verbose = False) def createCmd(self, url): str ="" if self['method'] == 'remote': str = self['remotecmd'] % url else: str = self['localcmd'] % url return str class UrlGrabber: def __init__(self, historysize): # init self.urls = {} self.globalUrls = [] self.historysize = 5 # control self.setHistorysize(historysize) def setHistorysize(self, count): if count > 1: self.historysize = count def getHistorysize(self): return self.historysize def addUrl(self, bufferp,url ): global urlGrabSettings self.globalUrls.insert(0,{"buffer":bufferp, "url":url, "time":time.strftime(urlGrabSettings["time_format"])}) #Log urls only if we have set a log path. if urlGrabSettings['url_log']: try : index = self.globalUrls[0] logfile = os.path.expanduser(urlGrabSettings['url_log']) dout = open(logfile, "a") dout.write("%s %s %s\n" % (index['time'], index['buffer'], index['url'])) dout.close() except : print("failed to log url check that %s is valid path" % urlGrabSettings['url_log']) pass # check for buffer if not bufferp in self.urls: self.urls[bufferp] = [] # add url if url in self.urls[bufferp]: self.urls[bufferp].remove(url) self.urls[bufferp].insert(0, url) # removing old urls while len(self.urls[bufferp]) > self.historysize: self.urls[bufferp].pop() def hasIndex( self, bufferp, index ): return bufferp in self.urls and \ len(self.url[bufferp]) >= index def hasBuffer( self, bufferp ): return bufferp in self.urls def getUrl(self, bufferp, index): url = "" if bufferp in self.urls: if len(self.urls[bufferp]) >= index: url = self.urls[bufferp][index-1] return url def prnt(self, buff): found = True if buff in self.urls: if len(self.urls[buff]) > 0: i = 1 for url in self.urls[buff]: urlGrabPrint("--> " + str(i) + " : " + url) i += 1 else: found = False elif buff == "*": for b in self.urls.keys(): self.prnt(b) else: found = False if not found: urlGrabPrint(buff + ": no entries") def urlGrabCheckMsgline(bufferp, message, isdisplayed): global urlGrab, max_buffer_length if not message or isdisplayed == 0: return # Ignore output from 'tinyurl.py' and our selfs if ( message.startswith( "[AKA] http://tinyurl.com" ) or message.startswith("[urlgrab]") ): return # Check for URLs for url in urlRe.findall(message): urlGrab.addUrl(bufferp,stripParens(url)) if max_buffer_length < len(bufferp): max_buffer_length = len(bufferp) if urlgrab_buffer: refresh() def urlGrabCheck(data, bufferp, uber_empty, tagsn, isdisplayed, ishilight, prefix, message): urlGrabCheckMsgline(hashBufferName(bufferp), message, isdisplayed) return weechat.WEECHAT_RC_OK def urlGrabCopy(bufferd, index): global urlGrab if bufferd == "": urlGrabPrint( "No current channel, you must activate one" ) elif not urlGrab.hasBuffer(bufferd): urlGrabPrint("No URL found - Invalid channel") else: if index <= 0: urlGrabPrint("No URL found - Invalid index") return url = urlGrab.getUrl(bufferd,index) if url == "": urlGrabPrint("No URL found - Invalid index") else: try: pipe = os.popen(urlGrabSettings['copycmd'],"w") pipe.write(url) pipe.close() urlGrabPrint("Url: %s gone to clipboard." % url) except: urlGrabPrint("Url: %s failed to copy to clipboard." % url) def urlGrabOpenUrl(url): global urlGrab, urlGrabSettings argl = urlGrabSettings.createCmd( quote(url, '/:#%?&+=') ) weechat.hook_process(argl,60000, "ug_open_cb", "") def ug_open_cb(data, command, code, out, err): #print out #print err return weechat.WEECHAT_RC_OK def urlGrabOpen(bufferd, index): global urlGrab, urlGrabSettings if bufferd == "": urlGrabPrint( "No current channel, you must specify one" ) elif not urlGrab.hasBuffer(bufferd) : urlGrabPrint("No URL found - Invalid channel") else: if index <= 0: urlGrabPrint("No URL found - Invalid index") return url = urlGrab.getUrl(bufferd,index) if url == "": urlGrabPrint("No URL found - Invalid index") else: urlGrabPrint("loading %s %sly" % (url, urlGrabSettings["method"])) urlGrabOpenUrl (url) def urlGrabList( args ): global urlGrab if len(args) == 0: buf = hashBufferName(weechat.current_buffer()) else: buf = args[0] if buf == "" or buf == "all": buf = "*" urlGrab.prnt(buf) def urlGrabMain(data, bufferp, args): if args[0:2] == "**": keyEvent(data, bufferp, args[2:]) return weechat.WEECHAT_RC_OK bufferd = hashBufferName(bufferp) largs = args.split(" ") #strip spaces while '' in largs: largs.remove('') while ' ' in largs: largs.remove(' ') if len(largs) == 0 or largs[0] == "show": if not urlgrab_buffer: init() refresh() weechat.buffer_set(urlgrab_buffer, "display", "1") elif largs[0] == 'open' and len(largs) == 2: urlGrabOpenUrl(largs[1]) elif largs[0] == 'list': urlGrabList( largs[1:] ) elif largs[0] == 'copy': if len(largs) > 1: no = int(largs[1]) urlGrabCopy(bufferd, no) else: urlGrabCopy(bufferd,1) else: try: no = int(largs[0]) if len(largs) > 1: urlGrabOpen(largs[1], no) else: urlGrabOpen(bufferd, no) except ValueError: #not a valid number so try opening it as a url.. for url in urlRe.findall(largs[0]): urlGrabOpenUrl(stripParens(url)) urlGrabPrint( "Unknown command '%s'. Try '/help url' for usage" % largs[0]) return weechat.WEECHAT_RC_OK def buffer_input(*kwargs): return weechat.WEECHAT_RC_OK def buffer_close(*kwargs): global urlgrab_buffer urlgrab_buffer = None return weechat.WEECHAT_RC_OK def keyEvent (data, bufferp, args): global urlGrab , urlGrabSettings, urlgrab_buffer, current_line if args == "refresh": refresh() elif args == "up": if current_line > 0: current_line = current_line -1 refresh_line (current_line + 1) refresh_line (current_line) ugCheckLineOutsideWindow() elif args == "down": if current_line < len(urlGrab.globalUrls) - 1: current_line = current_line +1 refresh_line (current_line - 1) refresh_line (current_line) ugCheckLineOutsideWindow() elif args == "scroll_top": temp_current = current_line current_line = 0 refresh_line (temp_current) refresh_line (current_line) weechat.command(urlgrab_buffer, "/window scroll_top") pass elif args == "scroll_bottom": temp_current = current_line current_line = len(urlGrab.globalUrls) refresh_line (temp_current) refresh_line (current_line) weechat.command(urlgrab_buffer, "/window scroll_bottom") elif args == "enter": if urlGrab.globalUrls[current_line]: urlGrabOpenUrl (urlGrab.globalUrls[current_line]['url']) def refresh_line (y): global urlGrab , urlGrabSettings, urlgrab_buffer, current_line, max_buffer_length #Print format Time(space)buffer(max4 spaces, but lined up)url format = "%%s%%s %%s%%-%ds%%s%%s" % (max_buffer_length+4) #non selected colors color_buffer = urlGrabSettings["color_buffer"] color_url = urlGrabSettings["color_url"] color_time =urlGrabSettings["color_time"] #selected colors color_buffer_selected = urlGrabSettings["color_buffer_selected"] color_url_selected = urlGrabSettings["color_url_selected"] color_time_selected = urlGrabSettings["color_time_selected"] color_bg_selected = urlGrabSettings["color_bg_selected"] color1 = color_time color2 = color_buffer color3 = color_url #If this line is selected we change the colors. if y == current_line: color1 = "%s,%s" % (color_time_selected, color_bg_selected) color2 = "%s,%s" % (color_buffer_selected, color_bg_selected) color3 = "%s,%s" % (color_url_selected, color_bg_selected) color1 = weechat.color(color1) color2 = weechat.color(color2) color3 = weechat.color(color3) text = format % (color1, urlGrab.globalUrls[y]['time'], color2, urlGrab.globalUrls[y]['buffer'], color3, urlGrab.globalUrls[y]['url'] ) weechat.prnt_y(urlgrab_buffer,y,text) def ugCheckLineOutsideWindow(): global urlGrab , urlGrabSettings, urlgrab_buffer, current_line, max_buffer_length if (urlgrab_buffer): infolist = weechat.infolist_get("window", "", "current") if (weechat.infolist_next(infolist)): start_line_y = weechat.infolist_integer(infolist, "start_line_y") chat_height = weechat.infolist_integer(infolist, "chat_height") if(start_line_y > current_line): weechat.command(urlgrab_buffer, "/window scroll -%i" %(start_line_y - current_line)) elif(start_line_y <= current_line - chat_height): weechat.command(urlgrab_buffer, "/window scroll +%i"%(current_line - start_line_y - chat_height + 1)) weechat.infolist_free(infolist) def refresh(): global urlGrab y=0 for x in urlGrab.globalUrls: refresh_line (y) y += 1 def init(): global urlGrab , urlGrabSettings, urlgrab_buffer if not urlgrab_buffer: urlgrab_buffer = weechat.buffer_new("urlgrab", "buffer_input", "", "buffer_close", "") if urlgrab_buffer: weechat.buffer_set(urlgrab_buffer, "type", "free") weechat.buffer_set(urlgrab_buffer, "key_bind_ctrl-R", "/url **refresh") weechat.buffer_set(urlgrab_buffer, "key_bind_meta2-A", "/url **up") weechat.buffer_set(urlgrab_buffer, "key_bind_meta2-B", "/url **down") weechat.buffer_set(urlgrab_buffer, "key_bind_meta-ctrl-J", "/url **enter") weechat.buffer_set(urlgrab_buffer, "key_bind_meta-ctrl-M", "/url **enter") weechat.buffer_set(urlgrab_buffer, "key_bind_meta-meta2-1./~", "/url **scroll_top") weechat.buffer_set(urlgrab_buffer, "key_bind_meta-meta2-4~", "/url **scroll_bottom") weechat.buffer_set(urlgrab_buffer, "title","Lists the urls in the applications") weechat.buffer_set(urlgrab_buffer, "display", "1") def completion_urls_cb(data, completion_item, bufferp, completion): """ Complete with URLS, for command '/url'. """ global urlGrab bufferd = hashBufferName( bufferp) for url in urlGrab.globalUrls : if url['buffer'] == bufferd: weechat.completion_list_add(completion, url['url'], 0, weechat.WEECHAT_LIST_POS_SORT) return weechat.WEECHAT_RC_OK def ug_unload_script(): """ Function called when script is unloaded. """ global urlGrabSettings weechat.config_write(urlGrabSettings.config_file) return weechat.WEECHAT_RC_OK #Main stuff if ( import_ok and weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,SCRIPT_DESC, "ug_unload_script", "") ): urlgrab_buffer = None current_line = 0 max_buffer_length = 0 urlGrabSettings = UrlGrabSettings() urlGrab = UrlGrabber( urlGrabSettings['historysize']) weechat.hook_print("", "", "", 1, "urlGrabCheck", "") weechat.hook_command(SCRIPT_COMMAND, "Url Grabber", "[open | | show | copy [n] | [n] | list]", "open or : opens the url\n" "show: Opens the select buffer to allow for url selection\n" "copy: Copies the nth url to the system clipboard\n" "list: Lists the urls in the current buffer\n", "open %(urlgrab_urls) || %(urlgrab_urls) || " "copy || show || list", "urlGrabMain", "") weechat.hook_completion("urlgrab_urls", "list of URLs", "completion_urls_cb", "") else: print("failed to load weechat") weechat-scripts/python/title.py0000644000175100017510000001027615112622376015676 0ustar manumanu''' Title-setter ''' # -*- coding: utf-8 -*- # # Copyright (c) 2009 by xt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Set screen title # (this script requires WeeChat 0.3.0 or newer) # # History: # 2016-03-31, devkev # version 0.9, Make hotlist optional # 2016-05-01, Ferus # version 0.8, Add ability to prefix and suffix the title, current # buffer, and hotlist buffers. As well as specify hotlist separator # 2016-02-05, ixti # version 0.7, Add Python3 support # 2015-06-07, t3chguy # version 0.6, Strip Colour Codes from Title # 2012-12-09, WakiMiko # version 0.5, update title when switching window (for WeeChat >= 0.3.7) # 2009-06-18, xt # version 0.4, option to use short_name # 2009-06-15, xt # version 0.3, free infolist # 2009-05-15, xt # version 0.2: add names from hotlist to title # 2009-05-10, xt # version 0.1: initial release import weechat as w SCRIPT_NAME = "title" SCRIPT_AUTHOR = "xt " SCRIPT_VERSION = "0.9" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Set screen title to current buffer name + hotlist items with configurable priority level" # script options settings = { "title_priority" : '2', "short_name" : 'on', "hotlist_separator" : ':', "title_prefix" : '[WeeChat ${info:version}] ', "title_suffix" : '', "hotlist_number_prefix": '', "hotlist_number_suffix": '', "hotlist_buffer_prefix": '', "hotlist_buffer_suffix": '', "current_buffer_prefix": '', "current_buffer_suffix": '', "show_hotlist" : 'on', } hooks = ( 'buffer_switch', 'window_switch', 'hotlist_*', ) def update_title(data, signal, signal_data): ''' The callback that adds title. ''' # prefix title = w.config_get_plugin('title_prefix') # current buffer title += w.config_get_plugin('current_buffer_prefix') if w.config_get_plugin('short_name') == 'on': title += w.buffer_get_string(w.current_buffer(), 'short_name') else: title += w.buffer_get_string(w.current_buffer(), 'name') title += w.config_get_plugin('current_buffer_suffix') if w.config_get_plugin('show_hotlist') == 'on': # hotlist buffers hotlist = w.infolist_get('hotlist', '', '') pnumber = w.config_get_plugin('hotlist_number_prefix') snumber = w.config_get_plugin('hotlist_number_suffix') pname = w.config_get_plugin('hotlist_buffer_prefix') sname = w.config_get_plugin('hotlist_buffer_suffix') separator = w.config_get_plugin('hotlist_separator') while w.infolist_next(hotlist): priority = w.infolist_integer(hotlist, 'priority') if priority >= int(w.config_get_plugin('title_priority')): number = w.infolist_integer(hotlist, 'buffer_number') thebuffer = w.infolist_pointer(hotlist, 'buffer_pointer') name = w.buffer_get_string(thebuffer, 'short_name') title += ' {0}{1}{2}{3}{4}{5}{6}'.format(pnumber, \ number, snumber, separator, pname, name, sname) w.infolist_free(hotlist) # suffix title += w.config_get_plugin('title_suffix') title = w.string_remove_color(title, '') w.window_set_title(title) return w.WEECHAT_RC_OK if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) for hook in hooks: w.hook_signal(hook, 'update_title', '') update_title('', '', '') weechat-scripts/python/autojoin_on_invite.py0000644000175100017510000001444015112622377020455 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2009 by xt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # (this script requires WeeChat 0.3.0 or newer) # # History: # 2022-10-19, Guillermo Castro # version 0.9: fix regex parsing of INVITE to allow non-ircv3 matching # 2022-03-2, h-0-s-h # version 0.8: fix reged parsing of INVITE message to account for ircv3/CAPS style/etc # (@time=2022-03-02T19:00:30.041Z :XXXXX!~XXXXX@xxxxx INVITE h-0-s-h :#xxxxx) # 2018-10-03, Pol Van Aubel # version 0.7: Python3 compatibility. Considerations: # - Settings during registration are iterated over only once, so # code should be succinct rather than efficient on both Py2/3. # - The autojoin_keys zip is turned into a dict, so wouldn't # make sense to first turn into a list as futurize suggests. # 2015-10-11, Simmo Saan # version 0.6: allow joining channels with keys in autojoin # 2013-12-21, Sebastien Helleu # version 0.5: fix parsing of INVITE message # 2013-11-28, sakkemo # version 0.4: add whitelist for nicks/channels # 2009-11-09, xt # version 0.3: add ignore option for channels # 2009-10-29, xt # version 0.2: add ignore option # 2009-10-28, xt # version 0.1: initial release import weechat as w import re SCRIPT_NAME = "autojoin_on_invite" SCRIPT_AUTHOR = "xt " SCRIPT_VERSION = "0.9" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Auto joins channels when invited" # script options settings = { 'whitelist_nicks': '', # comma separated list of nicks, # overrides ignore_nicks 'whitelist_channels': '', # comma separated list of channels, # overrides ignore_channels 'ignore_nicks': '', # comma separated list of nicks #that we will not accept auto invite from 'ignore_channels': '', # comma separated list of channels to not join 'autojoin_key': 'on', # use channel keys from server's autojoin list } if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): for option, default_value in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_value) w.hook_signal('*,irc_in2_invite', 'invite_cb', '') def join(server, channel): key = None if w.config_string_to_boolean(w.config_get_plugin('autojoin_key')): autojoin = w.config_string(w.config_get('irc.server.%s.autojoin' % server)).split(' ', 1) if len(autojoin) > 1: # any keys specified autojoin_keys = dict(zip(autojoin[0].split(','), autojoin[1].split(','))) key = autojoin_keys.get(channel) # defaults to None when not set if key: w.command('', '/quote -server %s JOIN %s %s' % (server, channel, key)) else: w.command('', '/quote -server %s JOIN %s' % (server, channel)) def invite_cb(data, signal, signal_data): server = signal.split(',')[0] # EFNet,irc_in_INVITE channel = signal_data.split()[-1].lstrip(':') # :nick!ident@host.name INVITE yournick :#channel from_nick = '' SearchStr = '(?:\@.*)?:(?P.+)!' #@time=2022-03-02T19:00:30.041Z :XX-XXXX!~XX-XXXX@xx.xxxx INVITE yournick :#xxxx-xxx (works also when no message-tag is present) from_nick = re.search(SearchStr, signal_data).groups()[0] if len(w.config_get_plugin('whitelist_nicks')) > 0 and len(w.config_get_plugin('whitelist_channels')) > 0: # if there's two whitelists, accept both if from_nick in w.config_get_plugin('whitelist_nicks').split(',') or channel in w.config_get_plugin('whitelist_channels').split(','): w.prnt('', 'Automatically joining %s on server %s, invitation from %s (whitelist).' \ %(channel, server, from_nick)) join(server, channel) else: w.prnt('', 'Ignoring invite from %s to channel %s. Neither inviter nor channel in whitelist.' %(from_nick, channel)) elif len(w.config_get_plugin('whitelist_nicks')) > 0: # if there's a whitelist, accept nicks in it if from_nick in w.config_get_plugin('whitelist_nicks').split(','): w.prnt('', 'Automatically joining %s on server %s, invitation from %s (whitelist).' \ %(channel, server, from_nick)) join(server, channel) else: w.prnt('', 'Ignoring invite from %s to channel %s. Inviter not in whitelist.' %(from_nick, channel)) elif len(w.config_get_plugin('whitelist_channels')) > 0: # if there's a whitelist, accept channels in it if channel in w.config_get_plugin('whitelist_channels').split(','): w.prnt('', 'Automatically joining %s on server %s, invitation from %s (whitelist).' \ %(channel, server, from_nick)) join(server, channel) else: w.prnt('', 'Ignoring invite from %s to channel %s. Channel not in whitelist.' %(from_nick, channel)) else: # use the ignore lists to make the decision if from_nick in w.config_get_plugin('ignore_nicks').split(','): w.prnt('', 'Ignoring invite from %s to channel %s. Invite from ignored inviter.' %(from_nick, channel)) elif channel in w.config_get_plugin('ignore_channels').split(','): w.prnt('', 'Ignoring invite from %s to channel %s. Invite to ignored channel.' %(from_nick, channel)) else: w.prnt('', 'Automatically joining %s on server %s, invitation from %s.' \ %(channel, server, from_nick)) join(server, channel) return w.WEECHAT_RC_OK weechat-scripts/python/spotify_nowplaying.py0000644000175100017510000001114015112622401020475 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2011 by agreeabledragon # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # (this script requires Spotify for Mac v0.5.1.98 or newer) # # History: # # 2022-01-25, Sébastien Helleu # version 0.1.2: fix mixed spaces and tabs for indentation # # 2011-06-12, agreeabledragon # version 0.1.1: rewrote it to use weechat.hook_process() to prevent it from blocking weechat as requested by Sébastien # # 2011-06-12, agreeabledragon # version 0.1: initial release # # @TODO: add options for customizing the output import weechat as w, re, subprocess, sys SCRIPT_NAME = "spotify_nowplaying" SCRIPT_AUTHOR = "agreeabledragon " SCRIPT_VERSION = "0.1.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Current song script for Spotify (v0.5.1.98 or newer) on OS X" SCRIPT_COMMAND = "spotify" # For executing the script SCRIPT_TIMEOUT = 1500 SCRIPT_PROCESS = False SCRIPT_BUFFER = False if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "") and sys.platform == "darwin": w.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, "", "", "", "spotify_exec", "") else: w.prnt("", "WARNING: This now playing script for Spotify only works on OS X with Spotify version 0.5.1.98 (or newer)") def spotify_process(data, command, rc, stdout, stderr): global SCRIPT_BUFFER, SCRIPT_PROCESS if stderr: w.prnt("", "There was an error executing the script - make sure you meet the requirements (OS X with Spotify v0.5.1.98 or newer)") SCRIPT_BUFFER = False SCRIPT_PROCESS = False return w.WEECHAT_RC_ERROR else: w.command(SCRIPT_BUFFER, stdout) SCRIPT_BUFFER = False SCRIPT_PROCESS = False return w.WEECHAT_RC_OK def spotify_exec(data, buffer, args): global SCRIPT_TIMEOUT, SCRIPT_BUFFER, SCRIPT_PROCESS if SCRIPT_PROCESS: w.prnt("", "Please wait for the other command to finish") return w.WEECHAT_RC_ERROR else: script = """set AppleScript's text item delimiters to ASCII character 10 set spotify_active to false set theString to \\"/me is not currently running Spotify.\\" tell application \\"Finder\\" if (get name of every process) contains \\"Spotify\\" then set spotify_active to true end tell if spotify_active then set got_track to false tell application \\"Spotify\\" if player state is playing then set theTrack to name of the current track set theArtist to artist of the current track set theAlbum to album of the current track set isStarred to starred of the current track set got_track to true end if end tell set theString to \\"/me is not playing anything in Spotify.\\" if got_track then if isStarred then set theString to \\"/me is listening to one of my favorite tracks \\\\\\"\\" & theTrack & \\"\\\\\\" by \\" & theArtist & \\" (Album: \\" & theAlbum & \\")\\" else set theString to \\"/me is listening to \\\\\\"\\" & theTrack & \\"\\\\\\" by \\" & theArtist & \\" (Album: \\" & theAlbum & \\")\\" end if end if end if return theString""" SCRIPT_BUFFER = buffer SCRIPT_PROCESS = w.hook_process('arch -i386 osascript -e "' + script + '"', SCRIPT_TIMEOUT, "spotify_process", "") return w.WEECHAT_RC_OK weechat-scripts/python/samegame.py0000644000175100017510000004114715112622402016323 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2012 Sebastien Helleu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Samegame for WeeChat (http://en.wikipedia.org/wiki/SameGame). # (requires WeeChat >= 0.3.6) # # History: # # 2012-03-16, Sebastien Helleu : # version 0.2: add undo key and bonus +1000 when all blocks are removed # 2012-03-16, Sebastien Helleu : # version 0.1: initial release # SCRIPT_NAME = 'samegame' SCRIPT_AUTHOR = 'Sebastien Helleu ' SCRIPT_VERSION = '0.2' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = 'Samegame' SCRIPT_COMMAND = 'samegame' import_ok = True try: import weechat except ImportError: print('This script must be run under WeeChat.') print('Get WeeChat now at: http://www.weechat.org/') import_ok = False try: import random, copy except ImportError as message: print('Missing package(s) for %s: %s' % (SCRIPT_NAME, message)) import_ok = False samegame = { 'buffer' : '', 'board' : [], 'sizes' : ((15, 10), (25, 17)), 'size' : (15, 10), 'zoom' : 1, 'colors' : [], 'numcolors' : 3, 'score' : 0, 'end' : '', 'timer' : '', 'board_undo': None, 'score_undo': 0, } # script options samegame_settings_default = { 'colors' : ['blue,red,green,yellow,magenta,cyan', 'comma-separated list of 6 colors for blocks'], 'numcolors': ['3', 'number of colors to use for blocks (3-6)'], 'zoom' : ['', 'zoom for board (0-N, empty means automatic zoom according to size of window)'], 'speed' : ['40', 'speed of animation when blocks are falling and columns are trimmed away (0 = immediate, 500 = slow animation'], } samegame_settings = {} # mouse keys samegame_mouse_keys = { '@chat(python.samegame):button1': '/window ${_window_number};hsignal:samegame_mouse' } def samegame_display(clear=False): """Display status and board.""" global samegame if not samegame['buffer']: return if clear: weechat.buffer_clear(samegame['buffer']) spaces = ' ' * ((samegame['zoom'] + 1) * 2) # display status str_status = 'Board: %s%dx%d%s Colors: %s%d%s Score: %s%d' % (weechat.color('white'), samegame['size'][0], samegame['size'][1], weechat.color('chat'), weechat.color('white'), samegame['numcolors'], weechat.color('chat'), weechat.color('white'), samegame['score']) str_end = '%s%s' % (weechat.color('white'), samegame['end']) weechat.prnt_y(samegame['buffer'], 0, '%s %s' % (str_status, str_end)) # display board weechat.prnt_y(samegame['buffer'], 1, '%s┌%s┐' % (weechat.color('chat'), '─' * (samegame['size'][0] * ((samegame['zoom'] + 1) * 2)))) for y, line in enumerate(samegame['board']): str_line = '│' for color in line: if color < 0: str_color = 'default' else: str_color = samegame['colors'][color] str_line += '%s%s' % (weechat.color(',%s' % str_color), spaces) str_line += '%s│' % weechat.color('chat') for i in range (0, samegame['zoom'] + 1): weechat.prnt_y(samegame['buffer'], 2 + (y * (samegame['zoom'] + 1)) + i, str_line) weechat.prnt_y(samegame['buffer'], 1 + (samegame['size'][1] * (samegame['zoom'] + 1)) + 1, '%s└%s┘' % (weechat.color('chat'), '─' * (samegame['size'][0] * ((samegame['zoom'] + 1) * 2)))) def samegame_adjust_zoom(): """Choose zoom according to size of window.""" global samegame, samegame_settings samegame['zoom'] = -1 if samegame_settings['zoom']: try: samegame['zoom'] = int(samegame_settings['zoom']) except: samegame['zoom'] = -1 if samegame['zoom'] < 0: width = weechat.window_get_integer(weechat.current_window(), 'win_chat_width') height = weechat.window_get_integer(weechat.current_window(), 'win_chat_height') for i in range(10, -1, -1): if width >= samegame['size'][0] * ((i + 1) * 2) + 2 and height >= (samegame['size'][1] * (i + 1)) + 3: samegame['zoom'] = i break if samegame['zoom'] < 0: samegame['zoom'] = 0 def samegame_set_colors(): """Set list of colors using settings.""" global samegame, samegame_settings, samegame_settings_default samegame['colors'] = samegame_settings['colors'].split(',') if len(samegame['colors']) != 6: weechat.prnt('', '%ssamegame: invalid colors (list must have 6 colors)' % weechat.prefix('error')) samegame['colors'] = samegame_settings_default['colors'][0].split(',') def samegame_config_cb(data, option, value): """Called when a script option is changed.""" global samegame, samegame_settings pos = option.rfind('.') if pos > 0: name = option[pos+1:] if name in samegame_settings: samegame_settings[name] = value if name == 'colors': samegame_set_colors() elif name == 'numcolors': try: samegame['numcolors'] = int(value) except: pass elif name == 'zoom': samegame_adjust_zoom() samegame_display() return weechat.WEECHAT_RC_OK def samegame_new_game(): """Create a new game: initialize board and some variables.""" global samegame samegame['board'] = [] for y in range(0, samegame['size'][1]): line = [] for x in range(0, samegame['size'][0]): line.append(random.randint(0, samegame['numcolors'] - 1)) samegame['board'].append(line) samegame['score'] = 0 samegame['end'] = '' samegame_display() def samegame_change_size(add): """Change size of board.""" global samegame keys = sorted(samegame['sizes']) index = keys.index(samegame['size']) + add if index >= 0 and index < len(keys): samegame['size'] = keys[index] weechat.buffer_clear(samegame['buffer']) samegame_adjust_zoom() samegame_new_game() def samegame_input_buffer(data, buffer, input): """Input data in samegame buffer.""" global samegame if input: args = input.split(' ') if args[0] in ('n', 'new'): samegame_new_game() elif args[0] in ('q', 'quit'): weechat.buffer_close(samegame['buffer']) elif args[0] == '+': samegame_change_size(+1) elif args[0] == '-': samegame_change_size(-1) elif args[0].isdigit(): numcolors = int(args[0]) if numcolors >= 3 and numcolors <= 6: samegame['numcolors'] = numcolors samegame_new_game() return weechat.WEECHAT_RC_OK def samegame_close_buffer(data, buffer): """Called when samegame buffer is closed.""" global samegame if samegame['timer']: weechat.unhook(samegame['timer']) samegame['timer'] = '' samegame['buffer'] = '' return weechat.WEECHAT_RC_OK def samegame_init(): """Init samegame: create buffer, adjust zoom, new game.""" global samegame, samegame_settings if samegame['buffer']: return samegame['buffer'] = weechat.buffer_search('python', 'samegame') if not samegame['buffer']: samegame['buffer'] = weechat.buffer_new('samegame', 'samegame_input_buffer', '', 'samegame_close_buffer', '') if samegame['buffer']: weechat.buffer_set(samegame['buffer'], 'type', 'free') weechat.buffer_set(samegame['buffer'], 'title', 'Samegame | mouse: play, alt-n: new game, alt-+/-: adjust board zoom, alt-u: undo | ' 'Command line: (n)ew, +/-: change size, (q)uit, 3-6: number of colors') weechat.buffer_set(samegame['buffer'], 'key_bind_meta-n', '/samegame new') weechat.buffer_set(samegame['buffer'], 'key_bind_meta-+', '/samegame zoom') weechat.buffer_set(samegame['buffer'], 'key_bind_meta--', '/samegame dezoom') weechat.buffer_set(samegame['buffer'], 'key_bind_meta-u', '/samegame undo') try: samegame['numcolors'] = int(samegame_settings['numcolors']) except: pass if samegame['numcolors'] < 3: samegame['numcolors'] = 3 if samegame['numcolors'] > 6: samegame['numcolors'] = 6 if samegame['buffer']: samegame_adjust_zoom() samegame_new_game() def samegame_play_xy(board, x, y): """Play at (x,y) and return number of blocks removed.""" color = board[y][x] count = 1 board[y][x] = -1 if y > 0 and board[y-1][x] == color: count += samegame_play_xy(board, x, y - 1) if y < samegame['size'][1] - 1 and board[y+1][x] == color: count += samegame_play_xy(board, x, y + 1) if x > 0 and board[y][x-1] == color: count += samegame_play_xy(board, x - 1, y) if x < samegame['size'][0] - 1 and board[y][x+1] == color: count += samegame_play_xy(board, x + 1, y) return count def samegame_column_is_empty(x): """Return True if a column is empty.""" global samegame count = 0 for y in range(0, samegame['size'][1]): if samegame['board'][y][x] < 0: count += 1 return (count == samegame['size'][1]) def samegame_collapse_blocks(): """Collapse blocks.""" global samegame columns = [] for x in range(0, samegame['size'][0]): for y in range(0, samegame['size'][1] - 1): if samegame['board'][y][x] >= 0 and samegame['board'][y+1][x] < 0: columns.append(x) break if columns: for x in columns: for y in range(samegame['size'][1] - 1, 0, -1): if samegame['board'][y][x] < 0 and samegame['board'][y-1][x] >= 0: samegame['board'][y][x] = samegame['board'][y-1][x] samegame['board'][y-1][x] = -1 return True for x in range(0, samegame['size'][0] - 1): if samegame_column_is_empty(x) and not samegame_column_is_empty(x + 1): for x2 in range(x, samegame['size'][0] - 1): for y in range(0, samegame['size'][1]): samegame['board'][y][x2] = samegame['board'][y][x2+1] for y in range(0, samegame['size'][1]): samegame['board'][y][samegame['size'][0]-1] = -1 return True return False def samegame_count_color(board, color): """Count number of times a color is used in board.""" count = 0 for line in board: count += line.count(color) return count def samegame_check_end(): """Check if the game has ended (play is not possible with remaining blocks).""" global samegame board = copy.deepcopy(samegame['board']) for x in range(0, samegame['size'][0]): for y in range(0, samegame['size'][1]): if board[y][x] >= 0: if samegame_play_xy(board, x, y) >= 2: return False samegame['end'] = 'End of game!' blocks_remaining = (samegame['size'][0] * samegame['size'][1]) - samegame_count_color(samegame['board'], -1) if blocks_remaining == 0: samegame['end'] += ' ** CONGRATS! **' samegame['score'] += 1000 else: samegame['end'] += ' (%d blocks remaining)' % blocks_remaining return True def samegame_timer_cb(data, remaining_calls): """Timer for animation (blocks falling).""" global samegame if samegame_collapse_blocks(): samegame_display() else: weechat.unhook(samegame['timer']) samegame['timer'] = '' if samegame_check_end(): samegame_display() return weechat.WEECHAT_RC_OK def samegame_play(x, y): """Play at (x,y), and check if game has ended.""" global samegame, samegame_settings if samegame['board'][y][x] < 0: return board = copy.deepcopy(samegame['board']) count = samegame_play_xy(board, x, y) if count < 2: return samegame['board_undo'] = copy.deepcopy(samegame['board']) samegame['score_undo'] = samegame['score'] count = samegame_play_xy(samegame['board'], x, y) samegame['score'] += (count - 1) ** 2 delay = 50 try: delay = int(samegame_settings['speed']) except: delay = 50 if delay < 0: delay = 0 elif delay > 500: delay = 500 if delay == 0: while samegame_collapse_blocks(): pass samegame_check_end() else: samegame['timer'] = weechat.hook_timer(delay, 0, 0, 'samegame_timer_cb', '') samegame_display() return def samegame_cmd_cb(data, buffer, args): """The /samegame command.""" global samegame samegame_init() if samegame['buffer']: weechat.buffer_set(samegame['buffer'], 'display', '1') if args == 'new': samegame_new_game() elif args == 'zoom': samegame['zoom'] += 1 samegame_display(True) elif args == 'dezoom': if samegame['zoom'] > 0: samegame['zoom'] -= 1 samegame_display(True) if not samegame['end']: if args == 'undo': if samegame['board_undo']: samegame['board'] = copy.deepcopy(samegame['board_undo']) samegame['board_undo'] = None samegame['score'] = samegame['score_undo'] samegame['score_undo'] = 0 samegame_display() return weechat.WEECHAT_RC_OK def samegame_mouse_cb(data, hsignal, hashtable): """Mouse callback.""" global samegame if not samegame['end'] and not samegame['timer']: x = int(hashtable.get('_chat_line_x', '-1')) y = int(hashtable.get('_chat_line_y', '-1')) if x >= 0 and y >= 0: color = -1 if y >= 2: x = x // ((samegame['zoom'] + 1) * 2) y = (y - 2) // (samegame['zoom'] + 1) if y < samegame['size'][1] and x < samegame['size'][0]: color = samegame['board'][y][x] if color >= 0: samegame_play(x, y) return weechat.WEECHAT_RC_OK if __name__ == '__main__' and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): # set default settings for option, value in samegame_settings_default.items(): if weechat.config_is_set_plugin(option): samegame_settings[option] = weechat.config_get_plugin(option) else: weechat.config_set_plugin(option, value[0]) samegame_settings[option] = value[0] weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) samegame_set_colors() # mouse support weechat.key_bind('mouse', samegame_mouse_keys) weechat.hook_hsignal('samegame_mouse', 'samegame_mouse_cb', '') # detect config changes weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, 'samegame_config_cb', '') # add command weechat.hook_command(SCRIPT_COMMAND, 'Samegame.', '', 'Instructions:\n' '- click on a group of adjoining blocks of the same color to remove them from the screen\n' '- blocks that are no longer supported will fall down, and a column without any blocks will be trimmed away by other columns sliding to the left\n' '- your score is increased by (N-1)², where N is the number of blocks removed by your click\n' '- the game ends when you can not play any more.', '', 'samegame_cmd_cb', '') # if buffer already exists (after /upgrade), init samegame if weechat.buffer_search('python', 'samegame'): samegame_init() weechat-scripts/python/grep_filter.py0000644000175100017510000002570615112622405017054 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2015 by Simmo Saan # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # History: # # 2021-11-06, Sébastien Helleu # version 0.11: make script compatible with WeeChat >= 3.4 # (new parameters in function hdata_search) # 2019-06-07, Trygve Aaberge # version 0.10: remove newlines from command completion # 2015-10-04, Simmo Saan # version 0.9: fix text search imitation in filter # 2015-08-27, Simmo Saan # version 0.8: add documentation # 2015-08-25, Simmo Saan # version 0.7: mute filter add/del # 2015-08-25, Simmo Saan # version 0.6: imitate search settings in filter # 2015-08-25, Simmo Saan # version 0.5: option for bar item text # 2015-08-25, Simmo Saan # version 0.4: option for default state # 2015-08-25, Simmo Saan # version 0.3: allow toggling during search # 2015-08-25, Simmo Saan # version 0.2: add bar item for indication # 2015-08-25, Simmo Saan # version 0.1: initial script # """ Filter buffers automatically while searching them """ from __future__ import print_function SCRIPT_NAME = "grep_filter" SCRIPT_AUTHOR = "Simmo Saan " SCRIPT_VERSION = "0.11" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Filter buffers automatically while searching them" SCRIPT_REPO = "https://github.com/sim642/grep_filter" SCRIPT_COMMAND = SCRIPT_NAME SCRIPT_BAR_ITEM = SCRIPT_NAME SCRIPT_LOCALVAR = SCRIPT_NAME IMPORT_OK = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org/") IMPORT_OK = False import re # re.escape SETTINGS = { "enable": ("off", "enable automatically start filtering when searching"), "bar_item": ("grep", "text to show in bar item when filtering"), } KEYS = {"ctrl-G": "/%s toggle" % SCRIPT_COMMAND} def get_merged_buffers(ptr): """ Get a list of buffers which are merged with "ptr". """ weechat_version = int(weechat.info_get("version_number", "") or 0) hdata = weechat.hdata_get("buffer") buffers = weechat.hdata_get_list(hdata, "gui_buffers") if weechat_version >= 0x03040000: buffer = weechat.hdata_search( hdata, buffers, "${buffer.number} == ${value}", {}, {"value": str(weechat.hdata_integer(hdata, ptr, "number"))}, {}, 1, ) else: buffer = weechat.hdata_search( hdata, buffers, "${buffer.number} == %i" % weechat.hdata_integer(hdata, ptr, "number"), 1, ) nbuffer = weechat.hdata_move(hdata, buffer, 1) ret = [] while buffer: ret.append(weechat.hdata_string(hdata, buffer, "full_name")) if weechat.hdata_integer( hdata, buffer, "number" ) == weechat.hdata_integer(hdata, nbuffer, "number"): buffer = nbuffer nbuffer = weechat.hdata_move(hdata, nbuffer, 1) else: buffer = None return ret def filter_exists(name): """ Check whether a filter named "name" exists. """ weechat_version = int(weechat.info_get("version_number", "") or 0) hdata = weechat.hdata_get("filter") filters = weechat.hdata_get_list(hdata, "gui_filters") if weechat_version >= 0x03040000: filter = weechat.hdata_search( hdata, filters, "${filter.name} == ${name}", {}, {"name": name}, {}, 1, ) else: filter = weechat.hdata_search( hdata, filters, "${filter.name} == %s" % name, 1, ) return bool(filter) def filter_del(name): """ Delete a filter named "name". """ weechat.command( weechat.buffer_search_main(), "/mute filter del %s" % name ) def filter_addreplace(name, buffers, tags, regex): """ Add (or replace if already exists) a filter named "name" with specified argumets. """ if filter_exists(name): filter_del(name) weechat.command( weechat.buffer_search_main(), "/mute filter add %s %s %s %s" % (name, buffers, tags, regex), ) def buffer_searching(buffer): """ Check whether "buffer" is in search mode. """ hdata = weechat.hdata_get("buffer") return bool(weechat.hdata_integer(hdata, buffer, "text_search")) def buffer_filtering(buffer): """ Check whether "buffer" should be filtered. """ local = weechat.buffer_get_string(buffer, "localvar_%s" % SCRIPT_LOCALVAR) return {"": None, "0": False, "1": True}[local] def buffer_build_regex(buffer): """ Build a regex according to "buffer"'s search settings. """ hdata = weechat.hdata_get("buffer") input = weechat.hdata_string(hdata, buffer, "input_buffer") exact = weechat.hdata_integer(hdata, buffer, "text_search_exact") where = weechat.hdata_integer(hdata, buffer, "text_search_where") regex = weechat.hdata_integer(hdata, buffer, "text_search_regex") if not regex: input = re.escape(input) if exact: input = "(?-i)%s" % input filter_regex = None if where == 1: # message filter_regex = input elif where == 2: # prefix filter_regex = "%s\\t" % input else: # prefix | message filter_regex = input # TODO: impossible with current filter regex return "!%s" % filter_regex def buffer_update(buffer): """ Refresh filtering in "buffer" by updating (or removing) the filter and update the bar item. """ hdata = weechat.hdata_get("buffer") buffers = ",".join(get_merged_buffers(buffer)) name = "%s_%s" % (SCRIPT_NAME, buffers) if buffer_searching(buffer): if buffer_filtering(buffer): filter_addreplace(name, buffers, "*", buffer_build_regex(buffer)) elif not buffer_filtering(buffer) and filter_exists(name): filter_del(name) elif filter_exists(name): filter_del(name) where = weechat.hdata_integer(hdata, buffer, "text_search_where") weechat.buffer_set( buffer, "localvar_set_%s_warn" % SCRIPT_LOCALVAR, "1" if where == 3 else "0", ) # warn about incorrect filter weechat.bar_item_update(SCRIPT_BAR_ITEM) def input_search_cb(data, signal, buffer): """ Handle "input_search" signal. """ if buffer_searching(buffer) and buffer_filtering(buffer) is None: enable = weechat.config_string_to_boolean( weechat.config_get_plugin("enable") ) weechat.buffer_set( buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "1" if enable else "0", ) weechat.buffer_set( buffer, "localvar_set_%s_warn" % SCRIPT_LOCALVAR, "0" ) elif not buffer_searching(buffer): weechat.buffer_set(buffer, "localvar_del_%s" % SCRIPT_LOCALVAR, "") weechat.buffer_set( buffer, "localvar_del_%s_warn" % SCRIPT_LOCALVAR, "" ) buffer_update(buffer) return weechat.WEECHAT_RC_OK def input_text_changed_cb(data, signal, buffer): """ Handle "input_text_changed" signal. """ if buffer_searching(buffer) and buffer_filtering(buffer): buffers = ",".join(get_merged_buffers(buffer)) name = "%s_%s" % (SCRIPT_NAME, buffers) filter_addreplace(name, buffers, "*", buffer_build_regex(buffer)) return weechat.WEECHAT_RC_OK def command_cb(data, buffer, args): """ Handle command. """ if args == "enable": weechat.buffer_set(buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "1") elif args == "disable": weechat.buffer_set(buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "0") elif args == "toggle": weechat.buffer_set( buffer, "localvar_set_%s" % SCRIPT_LOCALVAR, "0" if buffer_filtering(buffer) else "1", ) else: pass buffer_update(buffer) return weechat.WEECHAT_RC_OK def bar_item_cb(data, item, window, buffer, extra_info): """ Build the bar item's content for "buffer". """ buffers = ",".join(get_merged_buffers(buffer)) name = "%s_%s" % (SCRIPT_NAME, buffers) if filter_exists(name): warn = int( weechat.buffer_get_string( buffer, "localvar_%s_warn" % SCRIPT_LOCALVAR ) ) return "%s%s%s" % ( weechat.color("input_text_not_found" if warn else "bar_fg"), weechat.config_get_plugin("bar_item"), weechat.color("reset"), ) else: return "" if __name__ == "__main__" and IMPORT_OK: if weechat.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "", ): weechat.hook_signal("input_search", "input_search_cb", "") weechat.hook_signal("input_text_changed", "input_text_changed_cb", "") weechat.hook_command( SCRIPT_COMMAND, SCRIPT_DESC, """enable || disable || toggle""", """ enable: enable {0} in current buffer disable: disable {0} in current buffer toggle: toggle {0} in current buffer By default a bind in "search" context is added to toggle with "ctrl-G". To see {0} status during search, add "{1}" item to some bar. On default configuration you can do it with: /set weechat.bar.input.items "[input_prompt]+(away),[{1}],[input_search],[input_paste],input_text" Due to technical reasons with /filter it is not possible to exactly {0} in "pre|msg" search mode, thus the bar item is shown in warning color.""".format( SCRIPT_NAME, SCRIPT_BAR_ITEM ), """enable || disable || toggle""", "command_cb", "", ) weechat.bar_item_new("(extra)%s" % SCRIPT_BAR_ITEM, "bar_item_cb", "") for option, value in SETTINGS.items(): if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) weechat.config_set_desc_plugin( option, '%s (default: "%s")' % (value[1], value[0]) ) weechat.key_bind("search", KEYS) weechat-scripts/python/himan.py0000644000175100017510000001263115112622407015641 0ustar manumanu# Hi, man! and its versions are (c) 2019 by pX <@havok.org> (in #pX on EFNet) # Hi, man! was written under GPL3 license. http://gnu.org/licenses # The gist of the licnse: use and/or modify at your own risk! # # Hi, man! What have they been saying about you? # Log highlights to a private buffer while /away (or not!) # Hi, man! relies on words in weechat.look.highlight # https://github.com/px-havok/weechat-himan # # I wrote this because my good pal narcolept wanted a highmon.pl # that looked good with fUrlbuf (https://github.com/px-havok/weechat-furlbuf) # # Hi narcolept! # # History: # 04.09.2019: # v0.1: Initial release, py3-ok # 04.11.2019: # : Added 'notify' as an option # : Added hook_config so don't have to reload script for changes. try: import weechat as w except Exception: print('WeeChat (https://weechat.org/) required.') quit() SCRIPT_NAME = 'himan' SCRIPT_AUTHOR = 'pX @ havok' SCRIPT_VERSION = '0.1' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = "What have they been saying about you?" OPTIONS = {'buffer_color' : ("gray", 'color of buffer name'), 'nick_color' : ("gray", 'color of mentioners nick'), 'notify' : ("off", 'highlight (notify) buffer if written to'), 'only_away' : ("on", 'only log highlights while /away'), 'outp_left' : ("<", 'character(s) left of nick'), 'outp_left_color' : ("gray", 'color of character(s) left of nick'), 'outp_right' : (">", 'character(s) right of buffer name'), 'outp_right_color' : ("gray", 'color of character(s) right of nick'), 'outp_sep' : (" / ", 'nick/buffer separator(s)'), 'outp_sep_color' : ("gray", 'color of / buffer separator(s)'), } global rst rst = w.color('reset') # ================================[ item ]=============================== def c(color): return w.color(color) def cg(option): return w.config_get_plugin(option) def himan_buffer_create(): global himan_buffer himan_buffer = w.buffer_new('himan', 'himan_input_cb', '', '', '') w.buffer_set(himan_buffer, 'title', '-[Hi, man! v' + SCRIPT_VERSION + ']- ' + SCRIPT_DESC) w.buffer_set(himan_buffer, 'nicklist', '0') # configurable option to set buffer notify on or off w.buffer_set(himan_buffer, 'notify', '0') if cg('notify') == 'on': w.buffer_set(himan_buffer, 'notify', '1') def checker(data, buffer, date, tags, displayed, highlight, prefix, message): # Do nothing if no highlight words set if w.config_get('weechat.look.highlight') == '': return w.WEECHAT_RC_OK # if away logging is on but you're not away, do nothing if cg('only_away') == 'on' and not w.buffer_get_string(buffer, 'localvar_away'): return w.WEECHAT_RC_OK if int(highlight): tags = tags.split(',') nick = '' for idx in range(len(tags)): if 'nick_' in tags[idx]: nick = c(cg('nick_color')) + tags[idx][5:] + rst outp_left = c(cg('outp_left_color')) + cg('outp_left') + rst outp_right = c(cg('outp_right_color')) + cg('outp_right') + rst outp_sep = c(cg('outp_sep_color')) + cg('outp_sep') + rst buffername = c(cg('buffer_color')) + w.buffer_get_string(buffer, 'short_name') + rst sp = ' ' # account for ACTION (/me) if '*' in prefix: sp = ' * ' if not w.buffer_search('python', 'himan'): himan_buffer_create() w.prnt(himan_buffer, outp_left + nick + outp_sep + buffername + outp_right + sp + message) return w.WEECHAT_RC_OK # ===================[ weechat options & description ]=================== def init_options(): for option,value in list(OPTIONS.items()): if not w.config_is_set_plugin(option): w.config_set_plugin(option, value[0]) OPTIONS[option] = value[0] else: OPTIONS[option] = w.config_get_plugin(option) w.config_set_desc_plugin(option,'%s (default: "%s")' % (value[1], value[0])) # dummy input bar, does nothing. def himan_input_cb(data, buffer, input_data): return w.WEECHAT_RC_OK def timer_cb(data, remaining_calls): w.prnt(w.current_buffer(), '%s' % data) return w.WEECHAT_RC_OK # if notify option changes, update without reloading def notify_cb(data, option, value): option = cg('notify') if option == 'on': w.buffer_set(himan_buffer, 'notify', '1') elif option == 'off': w.buffer_set(himan_buffer, 'notify', '0') return w.WEECHAT_RC_OK def shutdown_cb(): global himan_buffer himan_buffer = None return w.WEECHAT_RC_OK # ================================[ main ]=============================== if __name__ == '__main__': if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, 'shutdown_cb', ''): init_options() himan_buffer_create() w.hook_timer(2000, 0, 1, 'timer_cb', '[himan]\tHi, man! What are they saying about you?\n' '[himan]\tHighlights will be logged to "himan" buffer\n' '[himan]\tOptions: /fset himan') w.hook_config("plugins.var.python." + SCRIPT_NAME + ".notify", "notify_cb", "") w.hook_print('', 'notify_message', '', 0, 'checker', '') weechat-scripts/python/url_olde.py0000644000175100017510000001546015112622406016354 0ustar manumanu# -*- coding: utf-8 -*- """ database init code from https://weechat.org/scripts/source/triggerreply.py.html regex is from http://stackoverflow.com/questions/520031/whats-the-cleanest-way-to-extract-urls-from-a-string-using-python TODO ---- - set a preference value for ignoring: - nicks - purge sql rows after an age range (or fixed size) """ SCRIPT_NAME = "url_olde" SCRIPT_AUTHOR = "Charlie Allom " SCRIPT_VERSION = "0.8" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "tells you how long ago a URL was first posted and by whom, for bragging rights." try: import weechat as w import sqlite3 import time import re from urllib.parse import urldefrag IMPORT_ERR = 0 except ImportError: IMPORT_ERR = 1 import os # plugins.var.python.url_olde.ignore_channel url_olde_settings_default = { 'ignored_channels': ('chanmon', 'comma separated list of buffers you want ignored. eg. freenode.#channelname') } url_olde_settings = {} def create_db(): """ create the sqlite database and insert a test URI as id 1 """ tmpcon = sqlite3.connect(DBFILE) cur = tmpcon.cursor() cur.execute("CREATE TABLE urls(id INTEGER PRIMARY KEY, uri VARCHAR, date INTEGER, nick VARCHAR, channel VARCHAR);") cur.execute("INSERT INTO urls(uri, date, nick, channel) VALUES ('test.com',1,'donald_trump','hello.#world');") tmpcon.commit() cur.close() def search_urls_cb(data, buffer, date, tags, displayed, highlight, prefix, message): """ searching for the url function message is the line that matched '://' buffer needs buffer_get_string for the short name prefix is nick """ database = sqlite3.connect(DBFILE) database.text_factory = str cursor = database.cursor() nick = prefix full_uri = re.findall(r'(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:/[^\s]*)?', message) channel = w.buffer_get_string(buffer, 'name') # current channel. # w.prnt(w.current_buffer(), 'full_uri: %s ' % (full_uri)) # debug for olde in full_uri: # iterate over each URI we get in the list from full_uri regex if '/#/' in olde: # run this routine on rails style apps url = olde uri = url.rstrip("/)") # strip the final / and lesser-seen ) new_entry = [] # create an ordered list of the following values we want to INSERT -> sql later on new_entry.append(uri) new_entry.append(time.time()) new_entry.append(nick) new_entry.append(channel) # w.prnt(w.current_buffer(), 'uri: %s ' % (uri)) # debug cursor.execute("SELECT date,uri,nick,channel from urls WHERE uri = ?", (uri,)) result = cursor.fetchone() if channel in str(url_olde_settings['ignored_channels']): # w.prnt(w.current_buffer(), 'ignoring %s due to ignored_channels = %s' % (uri, str(url_olde_settings['ignored_channels']))) return w.WEECHAT_RC_OK if result is None: """ a new URL is seen! """ # w.command(buffer, "/notice %s" % (new_entry)) # debug cursor.execute("INSERT INTO urls(uri, date, nick, channel) VALUES (?,?,?,?)", new_entry) database.commit() else: """ we've got a match from sqlite """ date, uri, nick, channel = result timestamp = time.strftime('%Y-%m-%d %H:%M', time.localtime(date)) # convert it to YYYY-MM-DD # w.command(buffer, "/notice DING %s" % str(result)) # debug w.prnt_date_tags(buffer, 0, 'no_log,notify_none', 'olde!! already posted by %s in %s on %s' % (nick, channel, timestamp)) else: # strip anchors url, fragment = urldefrag(olde) uri = url.rstrip("/)") # strip the final / and lesser-seen ) new_entry = [] # create an ordered list of the following values we want to INSERT -> sql later on new_entry.append(uri) new_entry.append(time.time()) new_entry.append(nick) new_entry.append(channel) # w.prnt(w.current_buffer(), 'uri: %s ' % (uri)) # debug cursor.execute("SELECT date,uri,nick,channel from urls WHERE uri = ?", (uri,)) result = cursor.fetchone() if channel in str(url_olde_settings['ignored_channels']): # w.prnt(w.current_buffer(), 'ignoring %s due to ignored_channels = %s' % (uri, str(url_olde_settings['ignored_channels']))) return w.WEECHAT_RC_OK if result is None: """ a new URL is seen! """ # w.command(buffer, "/notice %s" % (new_entry)) # debug cursor.execute("INSERT INTO urls(uri, date, nick, channel) VALUES (?,?,?,?)", new_entry) database.commit() else: """ we've got a match from sqlite """ date, uri, nick, channel = result timestamp = time.strftime('%Y-%m-%d %H:%M', time.localtime(date)) # convert it to YYYY-MM-DD # w.command(buffer, "/notice DING %s" % str(result)) # debug w.prnt_date_tags(buffer, 0, 'no_log,notify_none', 'olde!! already posted by %s in %s on %s' % (nick, channel, timestamp)) return w.WEECHAT_RC_OK def url_olde_load_config(): global url_olde_settings_default, url_olde_settings version = w.info_get('version_number', '') or 0 for option, value in url_olde_settings_default.items(): if w.config_is_set_plugin(option): url_olde_settings[option] = w.config_get_plugin(option) else: w.config_set_plugin(option, value[0]) url_olde_settings[option] = value[0] if int(version) >= 0x00030500: w.config_set_desc_plugin(option, value[1]) def url_olde_config_cb(data, option, value): """Called each time an option is changed.""" url_olde_load_config() return w.WEECHAT_RC_OK if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): if IMPORT_ERR: w.prnt("", "You need sqlite3 to run this plugin.") options = { 'directory': 'data', } DBFILE = w.string_eval_path_home("%h/olde.sqlite3", {}, {}, options) if not os.path.isfile(DBFILE): create_db() # init on load if file doesn't exist. # load the config url_olde_load_config() # config changes are reloaded w.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'url_olde_config_cb', '') # catch urls in buffer and send to the cb w.hook_print('', 'irc_privmsg', '://', 1, 'search_urls_cb', '') weechat-scripts/python/postpone.py0000644000175100017510000001225515112622400016407 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2010 by Alexander Schremmer # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # (this script requires WeeChat 0.3.0 or newer) # # History: # 2021-04-05, Sébastien Helleu # version 0.2.5: remove commented infolist code # 2019-07-24, Sébastien Helleu # version 0.2.4: make script compatible with Python 3 # 2015-04-29, Colgate Minuette # version 0.2.3: add option to send queued messages on /nick # 2013-11-08, Stefan Huber # version 0.2.2: add match_prefix setting, recall timestamp of message # 2012-12-29, Stefan Huber # version 0.2.1: fix channel determination in join_cb # 2010-05-20, Alexander Schremmer # version 0.2: removed InfoList code # 2010-05-15, Alexander Schremmer # version 0.1: initial release import weechat as w import re from datetime import datetime from time import strftime SCRIPT_NAME = "postpone" SCRIPT_AUTHOR = "Alexander Schremmer " SCRIPT_VERSION = "0.2.5" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Postpones written messages for later dispatching if target nick is not on channel" postpone_data = {} settings = { 'match_prefix': ('', 'Postpone message if prefix before "nick:" is matched. (Default: "")'), 'message_on_nick': ('off', 'Send message on /nick in addition to /join (Default: off)') } def send_messages(server, channel, nick): buffer = w.buffer_search("", "%s.%s" % (server, channel)) messages = postpone_data[server][channel][nick] for time, msg in messages: tstr = strftime("%Y-%m-%d %H:%M:%S", time.timetuple()) w.command(buffer, msg + " (This message has been postponed on " + tstr + ".)") messages[:] = [] def join_cb(data, signal, signal_data): server = signal.split(',')[0] # EFNet,irc_in_JOIN channel = re.match('.* JOIN :?(?P.+)$', signal_data).groups()[0] nick = re.match(':(?P.+)!', signal_data).groups()[0].lower() buffer = w.buffer_search("", "%s.%s" % (server, channel)) if server in postpone_data and channel in postpone_data[server] and\ nick in postpone_data[server][channel]: send_messages(server, channel, nick) return w.WEECHAT_RC_OK def nick_cb(data, signal, signal_data): if not w.config_is_set_plugin('message_on_nick'): return w.WEECHAT_RC_OK if not w.config_get_plugin('message_on_nick').lower() == "on": return w.WEECHAT_RC_OK server = signal.split(",")[0] if server in postpone_data: nick = signal_data.split(" ")[2] if nick.startswith(":"): nick = nick[1:] nick = nick.lower() for channel in postpone_data[server]: if nick in postpone_data[server][channel]: send_messages(server, channel, nick) return w.WEECHAT_RC_OK def channel_has_nick(server, channel, nick): buffer = w.buffer_search("", "%s.%s" % (server, channel)) return bool(w.nicklist_search_nick(buffer, "", nick)) def command_run_input(data, buffer, command): """ Function called when a command "/input xxxx" is run """ if command == "/input return": # As in enter was pressed. input_s = w.buffer_get_string(buffer, 'input') server = w.buffer_get_string(buffer, 'localvar_server') channel = w.buffer_get_string(buffer, 'localvar_channel') match_prefix = w.config_get_plugin('match_prefix') match = re.match(match_prefix + r'([\w-]+?): (.*)$', input_s) if match: nick, message = match.groups() if not channel_has_nick(server, channel, nick): w.prnt(buffer, "| Enqueued message for %s: %s" % (nick, message)) save = datetime.now(), nick + ": " + message postpone_data.setdefault(server, {}).setdefault(channel, {}).setdefault(nick.lower(), []).append(save) w.buffer_set(buffer, 'input', "") return w.WEECHAT_RC_OK if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): version = w.info_get('version_number', '') or 0 for option, default_desc in settings.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_desc[0]) if int(version) >= 0x00030500: w.config_set_desc_plugin(option, default_desc[1]) w.hook_command_run("/input return", "command_run_input", "") w.hook_signal('*,irc_in2_join', 'join_cb', '') w.hook_signal('*,irc_in2_nick', 'nick_cb', '') weechat-scripts/python/screen_away.py0000644000175100017510000002263215112622377017055 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2009 by xt # Copyright (c) 2009 by penryu # Copyright (c) 2010 by Blake Winton # Copyright (c) 2010 by Aron Griffis # Copyright (c) 2010 by Jani Kesänen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # (this script requires WeeChat 0.3.0 or newer) # # History: # 2019-03-04, Germain Z. # version 0.16: add option "socket_file", for use with e.g. dtach # : code reformatting for consistency/PEP8 # 2017-11-20, Nils Görs # version 0.15: make script python3 compatible # : fix problem with empty "command_on_*" options # : add option "no_output" # 2014-08-02, Nils Görs # version 0.14: add time to detach message. (idea by Mikaela) # 2014-06-19, Anders Bergh # version 0.13: Fix a simple typo in an option description. # 2014-01-12, Phyks (Lucas Verney) # version 0.12: Added an option to check status of relays to set unaway in # case of a connected relay. # 2013-08-30, Anders Einar Hilden # version: 0.11: Fix reading of set_away # 2013-06-16, Renato Botelho # version 0.10: add option to don't set away, only change nick # allow multiple commands on attach/dettach # do not add suffix if nick already have it # 2012-12-29, David Flatz # version 0.9: add option to ignore servers and don't set away status for them # add descriptions to config options # 2010-08-07, Filip H.F. "FiXato" Slagter # version 0.8: add command on attach feature # 2010-05-07, Jani Kesänen # version 0.7: add command on detach feature # 2010-03-07, Aron Griffis # version 0.6: move socket check to register, # add hook_config for interval, # reduce default interval from 60 to 5 # 2010-02-19, Blake Winton # version 0.5: add option to change nick when away # 2010-01-18, xt # version 0.4: only update servers that are connected # 2009-11-30, xt # version 0.3: do not touch servers that are manually set away # 2009-11-27, xt # version 0.2: code for TMUX from penryu # 2009-11-27, xt # version 0.1: initial release import os import re import time import weechat as w SCRIPT_NAME = 'screen_away' SCRIPT_AUTHOR = 'xt ' SCRIPT_VERSION = '0.16' SCRIPT_LICENSE = 'GPL3' SCRIPT_DESC = 'Set away status on screen detach' SETTINGS = { 'message': ('Detached from screen', 'Away message'), 'time_format': ('since %Y-%m-%d %H:%M:%S%z', 'time format append to away message'), 'interval': ('5', 'How often in seconds to check screen status'), 'away_suffix': ('', 'What to append to your nick when you\'re away.'), 'command_on_attach': ('', ('Commands to execute on attach, separated by ' 'semicolon')), 'command_on_detach': ('', ('Commands to execute on detach, separated by ' 'semicolon')), 'ignore': ('', 'Comma-separated list of servers to ignore.'), 'set_away': ('on', 'Set user as away.'), 'ignore_relays': ('off', 'Only check screen status and ignore relay interfaces'), 'no_output': ('off', 'no detach/attach information will be displayed in buffer'), 'socket_file': ('', 'Socket file to use (leave blank to auto-detect)'), } TIMER = None SOCK = None AWAY = False CONNECTED_RELAY = False def set_timer(): '''Update timer hook with new interval.''' global TIMER if TIMER: w.unhook(TIMER) TIMER = w.hook_timer(int(w.config_get_plugin('interval')) * 1000, 0, 0, 'screen_away_timer_cb', '') def screen_away_config_cb(data, option, value): '''Update timer / sock file on config changes.''' global SOCK if SOCK and option.endswith('.interval'): set_timer() elif option.endswith('.socket_file'): SOCK = value if not SOCK: SOCK = get_sock() if SOCK: set_timer() elif TIMER: w.unhook(TIMER) return w.WEECHAT_RC_OK def get_servers(): '''Get the servers that are not away, or were set away by this script.''' ignores = w.config_get_plugin('ignore').split(',') infolist = w.infolist_get('irc_server', '', '') buffers = [] while w.infolist_next(infolist): if (not w.infolist_integer(infolist, 'is_connected') == 1 or w.infolist_string(infolist, 'name') in ignores): continue if (not w.config_string_to_boolean(w.config_get_plugin('set_away')) or not w.infolist_integer(infolist, 'is_away') or w.config_get_plugin('message') in w.infolist_string( infolist, 'away_message')): buffers.append((w.infolist_pointer(infolist, 'buffer'), w.infolist_string(infolist, 'nick'))) w.infolist_free(infolist) return buffers def screen_away_timer_cb(buffer, args): '''Check if screen is attached and update awayness.''' global AWAY, SOCK, CONNECTED_RELAY set_away = w.config_string_to_boolean(w.config_get_plugin('set_away')) check_relays = not w.config_string_to_boolean( w.config_get_plugin('ignore_relays')) suffix = w.config_get_plugin('away_suffix') attached = os.access(SOCK, os.X_OK) # X bit indicates attached. # Check wether a client is connected on relay or not. CONNECTED_RELAY = False if check_relays: infolist = w.infolist_get('relay', '', '') if infolist: while w.infolist_next(infolist): status = w.infolist_string(infolist, 'status_string') if status == 'connected': CONNECTED_RELAY = True break w.infolist_free(infolist) if ((attached and AWAY) or (check_relays and CONNECTED_RELAY and not attached and AWAY)): if not w.config_string_to_boolean(w.config_get_plugin('no_output')): w.prnt('', '{}: Screen attached. Clearing away status'.format( SCRIPT_NAME)) for server, nick in get_servers(): if set_away: w.command(server, '/away') if suffix and nick.endswith(suffix): nick = nick[:-len(suffix)] w.command(server, '/nick {}'.format(nick)) AWAY = False if w.config_get_plugin('command_on_attach'): for cmd in w.config_get_plugin('command_on_attach').split(';'): w.command('', cmd) elif not attached and not AWAY: if not CONNECTED_RELAY: if (not w.config_string_to_boolean( w.config_get_plugin('no_output'))): w.prnt('', '{}: Screen detached. Setting away status'.format( SCRIPT_NAME)) for server, nick in get_servers(): if suffix and not nick.endswith(suffix): w.command(server, '/nick {}{}'.format(nick, suffix)) if set_away: w.command(server, '/away {} {}'.format( w.config_get_plugin('message'), time.strftime(w.config_get_plugin('time_format')))) AWAY = True if w.config_get_plugin('command_on_detach'): for cmd in w.config_get_plugin('command_on_detach').split(';'): w.command('', cmd) return w.WEECHAT_RC_OK def get_sock(): '''Try to get the appropriate sock file for screen/tmux.''' sock = None if 'STY' in os.environ.keys(): # We are running under screen. cmd_output = os.popen('env LC_ALL=C screen -ls').read() match = re.search(r'Sockets? in (/.+)\.', cmd_output) if match: sock = os.path.join(match.group(1), os.environ['STY']) if not sock and 'TMUX' in os.environ.keys(): # We are running under tmux. socket_data = os.environ['TMUX'] sock = socket_data.rsplit(',', 2)[0] return sock if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): version = w.info_get('version_number', '') or 0 for option, default_desc in SETTINGS.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, default_desc[0]) if int(version) >= 0x00030500: w.config_set_desc_plugin(option, default_desc[1]) SOCK = w.config_get_plugin('socket_file') if not SOCK: SOCK = get_sock() if SOCK: set_timer() w.hook_config('plugins.var.python.{}.*'.format(SCRIPT_NAME), 'screen_away_config_cb', '') weechat-scripts/python/irccloud_avatar_link.py0000644000175100017510000000327115112622410020716 0ustar manumanu# # Copyright (9) 2024 Jesse McDowell # # Add IRCCloud avatar image link to WHOIS output # # This program is free software: you can redistribute it and/or modify it under the terms of the # GNU General Public License as published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with this program. If not, # see . # # 2024-08-25: Jesse McDowell # version 1.0: Initial release try: import weechat from weechat import WEECHAT_RC_OK import_ok = True except ImportError: print('This script must be run under WeeChat.') import_ok = False import re def whois311_cb(data, signal, signal_data): buffer = weechat.info_get("irc_buffer", signal.split(",")[0]) userid_text = signal_data.split(" ")[5 if signal_data[0] == "@" else 4] userid_match = userid_expression.match(userid_text) if userid_match is not None: weechat.prnt(buffer, "Avatar image: https://static.irccloud-cdn.com/avatar-redirect/%s" % userid_match.groups()[0]) return WEECHAT_RC_OK if __name__ == '__main__' and import_ok: weechat.register("irccloud_avatar_link", "Jesse McDowell", "1.0", "GPL3", "Add IRCCloud avatar image link to WHOIS details", "", "") userid_expression = re.compile("^[us]id([0-9]+)$") weechat.hook_signal("*,irc_in2_311", "whois311_cb", "") weechat-scripts/python/collapse_channel.py0000644000175100017510000003532715112622407020046 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2019-2023 by nils_2 # # collapse channel buffers from servers without focus # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # 2019-03-13: nils_2, (freenode.#weechat) # 0.1 : initial release, py3k-ok # # 2019-03-19: nils_2, (freenode.#weechat) # 0.2 : add function exclude hotlist # # 2019-03-19: nils_2, (freenode.#weechat) # 0.3 : add function activity # # 2019-03-21: nils_2, (freenode.#weechat) # 0.4 : workaround for bug https://github.com/weechat/weechat/issues/1325#event-2214793184 # : workaround for signal buffer_switch, otherwise the warning "/allchan -current" will be printed # : add command help # : fix "/allchan -current" warning when /server raw is executed # # 2019-03-23: nils_2, (freenode.#weechat) # 0.5 : fix "/allchan -current" warning when signal "buffer_opened" is called # : changed default value for hotlist option # # 2019-05-09: nils_2, (freenode.#weechat) # 0.6 : fix hiding of channel buffer when private buffer opens # # 2019-09-06: nils_2, (freenode.#weechat) # 0.7 : fix: ignore "slack" for signal "buffer_switch" # # 2020-07-20: Sébastien Helleu # 0.8 : fix: add missing "/" in /allchan command # # 2021-11-06: Sébastien Helleu # 0.9 : make script compatible with WeeChat >= 3.4 # (new parameters in function hdata_search) # # 2023-09-01: nils_2, (libera.#weechat) # 1.0 : check for buffer_ptr and for irc buffer # # 2023-09-02: nils_2, (libera.#weechat) # 1.1 : one more check for buffer_ptr # # 2023-09-08: nils_2, (libera.#weechat) # 1.2 : when in non-irc buffers (eg. /server raw) exclude channels are ignored, internal changes # # 2024-11-16: nils_2, (libera.#weechat) # 1.3 : hook_signal(hotlist_changed) fixed. # idea and testing by DJ-ArcAngel try: import weechat,re except Exception: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org/") quit() SCRIPT_NAME = "collapse_channel" SCRIPT_AUTHOR = "nils_2 " SCRIPT_VERSION = "1.3" SCRIPT_LICENSE = "GPL" SCRIPT_DESC = "collapse channel buffers from servers without focus" OPTIONS = { 'server_exclude' : ('','exclude some server, comma separated list (wildcard "*" is allowed)'), 'channel_exclude' : ('','exclude some channel, comma separated list. This is server independent (wildcard "*" is allowed)'), 'single_channel_exclude': ('','exclude specific channels on specific server, space separated list (eg. freenode.#weechat)'), 'hotlist' : ('4','unhide buffer by activity, when buffer is added to hotlist (0=off, 1=message, 2=private message, 3=highlight, 4=all)'), 'activity' : ('off','show channels with activity only (see option hotlist). all exclude options will be ignored'), } # ================================[ buffer open/closed ]=============================== def buffer_opened_closed_cb(data, signal, signal_data): global OPTIONS # sadly localvar not set in this moment, when buffer opens! :-( # server = weechat.buffer_get_string(signal_data, 'localvar_server') # get internal servername infolist = weechat.infolist_get('buffer', signal_data, '') weechat.infolist_next(infolist) plugin_name = weechat.infolist_string(infolist, 'plugin_name') name = weechat.infolist_string(infolist, 'name') short_name = weechat.infolist_string(infolist, 'short_name') full_name = weechat.infolist_string(infolist, 'full_name') weechat.infolist_free(infolist) # TODO how about matrix script or other non-irc channel buffer? no idea! help is welcome if plugin_name != "irc": # for example /fset, /color etc.pp buffer return weechat.WEECHAT_RC_OK if OPTIONS['activity'].lower() == 'no' or OPTIONS['activity'].lower() == 'off' or OPTIONS['activity'].lower() == '0': weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) if not signal_data: # signal_data available? weechat.command(signal_data,'/allchan -current /buffer unhide') else: # signal_data empty! weechat.command('','/allchan /buffer hide') if signal_data and name.find('.') != -1: # signal_data available and "name" has separator "." eg "irc_raw" buffer? server = name.rsplit('.', 1)[-2] # server.buffer buffer_ptr = weechat.buffer_search('irc', 'server.%s' % server) if buffer_ptr: weechat.command(buffer_ptr,'/allchan -current /buffer unhide') weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) exclude_server() single_channel_exclude() else: weechat.command('','/allchan /buffer hide') exclude_hotlist() return weechat.WEECHAT_RC_OK # ============================[ buffer_switch ]=========================== def buffer_switch_cb(data, signal, signal_data): global OPTIONS, version plugin_name = weechat.buffer_get_string(signal_data, 'localvar_plugin') # get plugin if plugin_name != "irc": # script only support irc plugin! return weechat.WEECHAT_RC_OK # when you /join a buffer and irc.look.buffer_switch_join is ON, the new buffer pointer is not useable at this time weechat.command("","/wait 1ms /mute") server = weechat.buffer_get_string(signal_data, 'localvar_server') # get internal servername buffer_ptr = weechat.buffer_search('irc', 'server.%s' % server) # looks for server. (This does not effect eg server raw buffer) if not buffer_ptr and server != 'irc_raw': # buffer pointer exists? return weechat.WEECHAT_RC_OK # no! if OPTIONS['activity'].lower() == 'no' or OPTIONS['activity'].lower() == 'off' or OPTIONS['activity'].lower() == '0': # hide all channel but use -exclude weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) if server == 'irc_raw': # buffer is /server raw weechat.command('','/allchan /buffer unhide') weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) elif server != '': # a buffer with server weechat.command(buffer_ptr,'/allchan -current /buffer unhide') # use buffer pointer from server exclude_server() single_channel_exclude() else: if int(version) <= 0x02040000: # workaround weechat.command(signal_data,'/allchan -current /buffer hide') bufpointer = weechat.window_get_pointer(weechat.current_window(), 'buffer') # get current channel pointer weechat.command('','/allchan /buffer hide') weechat.command(bufpointer,'/buffer unhide') # unhide current channel exclude_hotlist() return weechat.WEECHAT_RC_OK # ================================[ hotlist changed ]============================== def hotlist_changed_cb(data, signal, signal_data): plugin_name = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_plugin') # TODO how about matrix script or other non-irc channel buffer? no idea! help is welcome # if plugin_name != 'irc': # script only support irc plugin! # return weechat.WEECHAT_RC_OK # weechat.command('', '/allchan /buffer hide') if OPTIONS['activity'].lower() == 'no' or OPTIONS['activity'].lower() == 'off' or OPTIONS['activity'].lower() == '0': exclude_server() single_channel_exclude() exclude_hotlist() return weechat.WEECHAT_RC_OK # ================================[ window switch ]=============================== def window_switch_cb(data, signal, signal_data): bufpointer = weechat.window_get_pointer(signal_data,'buffer') buffer_switch_cb(data,signal,bufpointer) return weechat.WEECHAT_RC_OK # ================================[ server signals ]=============================== def irc_server_disconnected_cb(data, signal, signal_data): buffer_switch_cb(data,signal,signal_data) return weechat.WEECHAT_RC_OK def irc_server_connected_cb(data, signal, signal_data): buffer_switch_cb(data,signal,signal_data) return weechat.WEECHAT_RC_OK def exclude_hotlist(): if OPTIONS['hotlist'] == '0' or OPTIONS['hotlist'] =='': return weechat.WEECHAT_RC_OK infolist = weechat.infolist_get('hotlist', '', '') while weechat.infolist_next(infolist): buffer_number = weechat.infolist_integer(infolist, 'buffer_number') priority = weechat.infolist_integer(infolist, 'priority') if int(OPTIONS['hotlist']) == priority or OPTIONS['hotlist'] == '4': weechat.command('','/buffer unhide %s' % buffer_number) weechat.infolist_free(infolist) return weechat.WEECHAT_RC_OK def exclude_server(): global OPTIONS for server_exclude in OPTIONS['server_exclude'].split(','): if server_exclude == '*': # show buffer for all server weechat.command('','/buffer unhide -all') # simply unload script, no!? :-) break # search exclude server in list of servers hdata = weechat.hdata_get('irc_server') servers = weechat.hdata_get_list(hdata, 'irc_servers') if int(version) >= 0x03040000: server = weechat.hdata_search( hdata, servers, '${irc_server.name} =* ${server_name}', {}, {'server_name': server_exclude}, {}, 1, ) else: server = weechat.hdata_search( hdata, servers, '${irc_server.name} =* %s' % server_exclude, 1, ) if server: # is_connected = weechat.hdata_integer(hdata, server, "is_connected") # nick_modes = weechat.hdata_string(hdata, server, "nick_modes") buffer_ptr = weechat.hdata_pointer(hdata, server, 'buffer') if buffer_ptr: # buffer pointer exists? weechat.command(buffer_ptr,'/allchan -current /buffer unhide') # yes! return def single_channel_exclude(): if OPTIONS['single_channel_exclude']: # space separated list for /buffer unhide weechat.command('','/buffer unhide %s' % OPTIONS['single_channel_exclude']) return # ================================[ weechat options & description ]=============================== def init_options(): for option,value in list(OPTIONS.items()): weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0])) if not weechat.config_is_set_plugin(option): weechat.config_set_plugin(option, value[0]) OPTIONS[option] = value[0] else: OPTIONS[option] = weechat.config_get_plugin(option) def toggle_refresh(pointer, name, value): global OPTIONS option = name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname OPTIONS[option] = value # save new value # TODO how about matrix script or other non-irc channel buffer? no idea! help is welcome server = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_server') server_ptr = weechat.buffer_search('irc', 'server.%s' % server) buffer_switch_cb('', '', server_ptr) return weechat.WEECHAT_RC_OK # unhide all buffers when script unloads def shutdown_cb(): weechat.command('', '/buffer unhide -all') return weechat.WEECHAT_RC_OK # ================================[ main ]=============================== if __name__ == "__main__": global version if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, 'shutdown_cb', ''): weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, '', "Note: channels from disconnected server will be displayed and won't hidden automatically.\n" '- This script only affects channels from irc plugin.\n' '- Use the /fset plugin to configure script: /fset collapse_channel', '', '', '') version = weechat.info_get('version_number', '') or 0 init_options() weechat.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' ) if OPTIONS['activity'].lower() == 'no' or OPTIONS['activity'].lower() == 'off' or OPTIONS['activity'].lower() == '0': # hide all channels weechat.command('','/allchan -exclude=%s /buffer hide' % OPTIONS['channel_exclude']) # show channel from current server server = weechat.buffer_get_string(weechat.current_buffer(), 'localvar_server') if server: weechat.command(server,'/allchan -current /buffer unhide') exclude_server() single_channel_exclude() else: weechat.command('','/allchan /buffer hide') exclude_hotlist() weechat.hook_signal('buffer_switch', 'buffer_switch_cb', '') weechat.hook_signal('buffer_opened', 'buffer_opened_closed_cb', '') weechat.hook_signal('buffer_closed', 'buffer_opened_closed_cb', '') weechat.hook_signal('window_switch', 'window_switch_cb', '') weechat.hook_signal('irc_server_connected', 'irc_server_connected_cb', '') weechat.hook_signal('irc_server_disconnected', 'irc_server_disconnected_cb', '') weechat.hook_signal('hotlist_changed', 'hotlist_changed_cb', '') weechat-scripts/python/log.py0000644000175100017510000001465315112622407015334 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (c) 2019 by nils_2 # # for easy toggling current buffer logging. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # 2019-06-24: nils_2, (freenode.#weechat) # 0.1 : initial version # # requires: WeeChat version 1.0 # # Development is currently hosted at # https://github.com/weechatter/weechat-scripts try: import weechat,re except Exception: print('This script must be run under WeeChat.') print('Get WeeChat now at: https://www.weechat.org/') quit() SCRIPT_NAME = 'log' SCRIPT_AUTHOR = 'nils_2 ' SCRIPT_VERSION = '0.1' SCRIPT_LICENSE = 'GPL' SCRIPT_DESC = 'for easy toggling current buffer logging' # eval_expression(): to match ${color:nn} tags regex_color=re.compile('\$\{color:([^\{\}]+)\}') # ==========================[ eval_expression() ]======================== def substitute_colors(text,window): global version if int(version) >= 0x00040200: buffer_ptr = weechat.window_get_pointer(window,"buffer") return weechat.string_eval_expression(text, {"window": window, "buffer": buffer_ptr}, {}, {}) # substitute colors in output return re.sub(regex_color, lambda match: weechat.color(match.group(1)), text) # ============================[ subroutines ]============================ def log_cmd_cb(data, buffer, args): argv = args.strip().split(' ') log_level = infolist_log_buffer(buffer) if args == "" or (argv[0].lower() == 'show'): # no args given. display log level of current buffer weechat.prnt(buffer,'log level: %s' % log_level) return weechat.WEECHAT_RC_OK if (argv[0].lower() == 'enable') or (argv[0].lower() == 'on'): if log_level != 'disabled': return weechat.WEECHAT_RC_OK # buffer already logging! else: enable_check(log_level,buffer) return weechat.WEECHAT_RC_OK if (argv[0].lower() == 'disable') or (argv[0].lower() == 'off'): if log_level == 'disabled': return weechat.WEECHAT_RC_OK # buffer already disabled! else: disable_check(log_level,buffer) return weechat.WEECHAT_RC_OK if (argv[0].lower() == 'toggle'): if log_level == 'disabled': enable_check(log_level,buffer) else: disable_check(log_level,buffer) return weechat.WEECHAT_RC_OK return weechat.WEECHAT_RC_OK # ===============================[ logger() ]============================= def enable_check(log_level,buffer): log_level = buffer_get_string_log_level(buffer) if log_level: if not str(log_level).isnumeric() or (int(log_level) < 0) or (int(log_level) > 9): log_level = 9 # invalid log level, set default weechat.command(buffer,'/logger set %s' % log_level) buffer_del_log_level(buffer) else: # no logging and no localvar. weechat.command(buffer,'/logger set 9') return weechat.WEECHAT_RC_OK def disable_check(log_level,buffer): buffer_set_string_log_level(buffer,log_level) # store old log level in localvar! weechat.command(buffer,'/logger disable') return weechat.WEECHAT_RC_OK # =============================[ localvars() ]============================ def buffer_get_string_log_level(buffer): return weechat.buffer_get_string(buffer,'localvar_log_level') def buffer_set_string_log_level(buffer,log_level): weechat.buffer_set(buffer, 'localvar_set_log_level', '%s' % log_level) return weechat.WEECHAT_RC_OK def buffer_del_log_level(buffer): weechat.command(buffer,'/buffer set localvar_del_log_level') return weechat.WEECHAT_RC_OK # =============================[ infolist() ]============================ def infolist_log_buffer(ptr_buffer): log_level = None infolist = weechat.infolist_get('logger_buffer','','') while weechat.infolist_next(infolist): bpointer = weechat.infolist_pointer(infolist, 'buffer') if ptr_buffer == bpointer: log_enabled = weechat.infolist_integer(infolist, 'log_enabled') log_level = weechat.infolist_integer(infolist, 'log_level') weechat.infolist_free(infolist) # free infolist() if not log_level: return 'disabled' else: return log_level # ================================[ main ]=============================== if __name__ == '__main__': global version if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''): version = weechat.info_get('version_number', '') or 0 # get weechat version (1.0) and store it if int(version) >= 0x01000000: weechat.hook_command(SCRIPT_NAME,SCRIPT_DESC, 'enable|on||' 'disable|off||' 'toggle||' 'show', ' enable/on: enable logging on current buffer, with default log-level (note: log-level from localvar will be used, if possible)\n' 'disable/off: disable logging on current buffer (note: log-level is stored in localvar)\n' ' toggle: will toggle logging on current buffer\n' ' show: will print current log-level to buffer (default)\n' '\n' 'Examples:\n' ' /log toggle', 'enable||' 'disable||' 'on||' 'off||' 'toggle||' 'show', 'log_cmd_cb', '') else: weechat.prnt('','%s%s %s' % (weechat.prefix('error'),SCRIPT_NAME,': needs version 1.0 or higher')) weechat.command('','/wait 1ms /python unload %s' % SCRIPT_NAME) weechat-scripts/python/giphy.py0000644000175100017510000000414715112622406015667 0ustar manumanu# -*- coding: utf-8 -*- # # Insert a giphy URL based on a command and search # Use giphys random, search and translate from weechat # Usage: /giphy search Search Term # Usage: /giphy msg message # Usage: /giphy random Search Term # Usage: /gipgy Search Term # # History: # # 2018-10-14, butlerx # Version 1.0.2: clean up code # 2017-04-19, butlerx # Version 1.0.1: remove + from message # 2017-04-18, butlerx # Version 1.0.0: initial version # from __future__ import absolute_import from requests import get from weechat import WEECHAT_RC_OK, command, hook_command, register SCRIPT_NAME = "giphy" SCRIPT_AUTHOR = "butlerx " SCRIPT_VERSION = "1.0.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Insert giphy gif" def giphy(data, buf, args): """ Parse args to decide what api to use """ search_string = args.split() arg = search_string.pop(0) search_string = "+".join(search_string) results = ( api_request("search", {"limit": 1, "q": search_string}) if arg == "search" else api_request("translate", {"s": search_string}) if arg == "msg" else api_request("random", {"tag": search_string}) if arg == "random" else api_request("random", {"tag": "+".join([arg, search_string])}) ) command( buf, "giphy {} -- {}".format(search_string.replace("+", " ").strip(), results) ) return WEECHAT_RC_OK def api_request(method, params): """Query giphy api for search""" try: params["api_key"] = "dc6zaTOxFJmzC" response = get("http://api.giphy.com/v1/gifs/{}".format(method), params=params) data = response.json()["data"] data = data[0] if isinstance(data, list) else data return ( data["images"]["original"]["url"] if "image_url" not in data else data["image_url"] ) except TypeError: return "No GIF good enough" if __name__ == "__main__": if register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" ): hook_command(SCRIPT_NAME, SCRIPT_DESC, "", "", "", SCRIPT_NAME, "") weechat-scripts/python/buffer_autoset.py0000644000175100017510000003256215112622400017560 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2010-2017 Sébastien Helleu # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # Auto-set buffer properties when a buffer is opened. # (this script requires WeeChat 1.0 or newer) # # History: # # 2021-06-02, Sébastien Helleu : # version 1.2: fix /help buffer_autoset # 2018-04-14, Kim B. Heino: # version 1.1: on startup apply settings to already opened buffers # 2017-06-21, Sébastien Helleu : # version 1.0: rename command /autosetbuffer to /buffer_autoset # 2015-09-28, Simmo Saan : # version 0.9: instantly apply properties # 2015-07-12, Sébastien Helleu : # version 0.8: add option buffer_autoset.look.timer to add a small timer # before setting buffer properties # 2015-04-05, Nils Görs : # version 0.7: increase priority of hook_signal('buffer_opened') # 2012-12-09, Nils Görs : # version 0.6: add support of core buffer # 2012-03-09, Sébastien Helleu : # version 0.5: fix reload of config file # 2012-01-03, Sébastien Helleu : # version 0.4: make script compatible with Python 3.x # 2010-12-02, Sébastien Helleu : # version 0.3: "no_highlight_nicks" replaced by "hotlist_max_level_nicks" # 2010-10-11, Sébastien Helleu : # version 0.2: add example in /help autosetbuffer with new buffer # property "no_highlight_nicks" # 2010-04-19, Sébastien Helleu : # version 0.1: initial release # SCRIPT_NAME = "buffer_autoset" SCRIPT_AUTHOR = "Sébastien Helleu " SCRIPT_VERSION = "1.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Auto-set buffer properties when a buffer is opened" SCRIPT_COMMAND = SCRIPT_NAME import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False CONFIG_FILE_NAME = "buffer_autoset" # config file / options bas_config_file = "" bas_options = {} # =================================[ config ]================================= def bas_config_init(): """ Initialization of configuration file. Sections: buffer. """ global bas_config_file, bas_options bas_config_file = weechat.config_new(CONFIG_FILE_NAME, "bas_config_reload_cb", "") if bas_config_file == "": return # section "look" section_look = weechat.config_new_section( bas_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", "") if not section_look: weechat.config_free(bas_config_file) return # options in section "look" bas_options["look_timer"] = weechat.config_new_option( bas_config_file, section_look, "timer", "integer", "Timer used to delay the set of properties (in milliseconds, " "0 = don't use a timer)", "", 0, 2147483647, "1", "1", 0, "", "", "", "", "", "") bas_options["look_instant"] = weechat.config_new_option( bas_config_file, section_look, "instant", "boolean", "Instantly apply properties to buffers affected", "", 0, 0, "on", "on", 0, "", "", "", "", "", "") # section "buffer" section_buffer = weechat.config_new_section( bas_config_file, "buffer", 1, 1, "", "", "", "", "", "", "bas_config_buffer_create_option_cb", "", "", "") if not section_buffer: weechat.config_free(bas_config_file) return def bas_config_buffer_create_option_cb(data, config_file, section, option_name, value): option = weechat.config_search_option(config_file, section, option_name) if option: return weechat.config_option_set(option, value, 1) else: option = weechat.config_new_option(config_file, section, option_name, "string", "", "", 0, 0, "", value, 0, "", "", "", "", "", "") if not option: return weechat.WEECHAT_CONFIG_OPTION_SET_ERROR return weechat.WEECHAT_CONFIG_OPTION_SET_OK_SAME_VALUE def bas_config_reload_cb(data, config_file): """Reload configuration file.""" return weechat.config_reload(config_file) def bas_config_read(): """Read configuration file.""" global bas_config_file return weechat.config_read(bas_config_file) def bas_config_write(): """Write configuration file.""" global bas_config_file return weechat.config_write(bas_config_file) # ================================[ command ]================================= def bas_cmd(data, buffer, args): """Callback for /buffer_autoset command.""" args = args.strip() if args == "": weechat.command("", "/set %s.buffer.*" % CONFIG_FILE_NAME) return weechat.WEECHAT_RC_OK argv = args.split(None, 3) if len(argv) > 0: if argv[0] == "add": if len(argv) < 4: weechat.command("", "/help %s" % SCRIPT_COMMAND) return weechat.WEECHAT_RC_OK weechat.command("", "/set %s.buffer.%s.%s \"%s\"" % (CONFIG_FILE_NAME, argv[1], argv[2], argv[3])) elif argv[0] == "del": if len(argv) < 2: weechat.command("", "/help %s" % SCRIPT_COMMAND) return weechat.WEECHAT_RC_OK weechat.command("", "/unset %s.buffer.%s" % (CONFIG_FILE_NAME, argv[1])) else: weechat.command("", "/help %s" % SCRIPT_COMMAND) return weechat.WEECHAT_RC_OK return weechat.WEECHAT_RC_OK def bas_completion_current_buffer_cb(data, completion_item, buffer, completion): """ Complete with current buffer name (plugin.name), for command '/buffer_autoset'. """ name = "%s.%s" % (weechat.buffer_get_string(buffer, "plugin"), weechat.buffer_get_string(buffer, "name")) weechat.hook_completion_list_add(completion, name, 0, weechat.WEECHAT_LIST_POS_BEGINNING) return weechat.WEECHAT_RC_OK def bas_completion_options_cb(data, completion_item, buffer, completion): """Complete with config options, for command '/buffer_autoset'.""" options = weechat.infolist_get("option", "", "%s.buffer.*" % CONFIG_FILE_NAME) if options: while weechat.infolist_next(options): weechat.hook_completion_list_add( completion, weechat.infolist_string(options, "option_name"), 0, weechat.WEECHAT_LIST_POS_SORT) weechat.infolist_free(options) return weechat.WEECHAT_RC_OK # ==========================[ timer/signal/option ]=========================== def bas_apply_options_for_buffer(buffer): full_name = weechat.buffer_get_string(buffer, "full_name") options = weechat.infolist_get("option", "", "%s.buffer.*" % CONFIG_FILE_NAME) if not options: return while weechat.infolist_next(options): option = weechat.infolist_string(options, "option_name") value = weechat.infolist_string(options, "value") if option: pos = option.rfind(".") if pos > 0: buffer_mask = option[0:pos] property = option[pos+1:] if buffer_mask and property: if weechat.string_match(full_name, buffer_mask, 1): weechat.buffer_set(buffer, property, value) weechat.infolist_free(options) def bas_timer_buffer_opened_cb(data, remaining_calls): full_name = data buffer = weechat.buffer_search("==", full_name) if not buffer: return weechat.WEECHAT_RC_OK bas_apply_options_for_buffer(buffer) return weechat.WEECHAT_RC_OK def bas_signal_buffer_opened_cb(data, signal, signal_data): global bas_options buffer = signal_data timer = weechat.config_integer(bas_options["look_timer"]) if timer == 0: bas_apply_options_for_buffer(buffer) else: weechat.hook_timer(timer, 0, 1, "bas_timer_buffer_opened_cb", weechat.buffer_get_string(buffer, "full_name")) return weechat.WEECHAT_RC_OK def bas_config_option_cb(data, option, value): if not weechat.config_boolean(bas_options["look_instant"]): return weechat.WEECHAT_RC_OK if not weechat.config_get(option): # option was deleted return weechat.WEECHAT_RC_OK option = option[len("%s.buffer." % CONFIG_FILE_NAME):] pos = option.rfind(".") if pos > 0: buffer_mask = option[0:pos] property = option[pos+1:] if buffer_mask and property: buffers = weechat.infolist_get("buffer", "", buffer_mask) if not buffers: return weechat.WEECHAT_RC_OK while weechat.infolist_next(buffers): buffer = weechat.infolist_pointer(buffers, "pointer") weechat.buffer_set(buffer, property, value) weechat.infolist_free(buffers) return weechat.WEECHAT_RC_OK # ==================================[ main ]================================== if __name__ == "__main__" and import_ok: if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "bas_unload_script", ""): version = weechat.info_get("version_number", "") or 0 if int(version) < 0x01000000: weechat.prnt("", "%s%s: WeeChat 1.0 is required for this script." % (weechat.prefix("error"), SCRIPT_NAME)) else: bas_config_init() bas_config_read() weechat.hook_command( SCRIPT_COMMAND, "Auto-set buffer properties when a buffer is opened", "[add buffer property value] | [del option]", " add: add a buffer/property/value in configuration file\n" " del: delete an option from configuration file\n" " buffer: name of a buffer (wildcard \"*\" is allowed)\n" "property: buffer property\n" " value: value for property\n" " option: name of option from configuration file\n\n" "Examples:\n" " disable timestamp on channel #weechat:\n" " /" + SCRIPT_COMMAND + " add irc.libera.#weechat " "time_for_each_line 0\n" " add word \"weechat\" in highlight list on channel " "#savannah:\n" " /" + SCRIPT_COMMAND + " add irc.libera.#savannah " "highlight_words_add weechat\n" " disable highlights from nick \"mike\" on libera server, " "channel #weechat (requires WeeChat >= 0.3.4):\n" " /" + SCRIPT_COMMAND + " add irc.libera.#weechat " "hotlist_max_level_nicks_add mike:2\n" " disable hotlist changes for nick \"bot\" on libera " "server (all channels) (requires WeeChat >= 0.3.4):\n" " /" + SCRIPT_COMMAND + " add irc.libera.* " "hotlist_max_level_nicks_add bot:-1", "add %(buffers_plugins_names)|" "%(buffer_autoset_current_buffer) " "%(buffer_properties_set)" " || del %(buffer_autoset_options)", "bas_cmd", "") weechat.hook_completion( "buffer_autoset_current_buffer", "current buffer name for buffer_autoset", "bas_completion_current_buffer_cb", "") weechat.hook_completion( "buffer_autoset_options", "list of options for buffer_autoset", "bas_completion_options_cb", "") weechat.hook_signal("9000|buffer_opened", "bas_signal_buffer_opened_cb", "") weechat.hook_config("%s.buffer.*" % CONFIG_FILE_NAME, "bas_config_option_cb", "") # apply settings to all already opened buffers buffers = weechat.infolist_get("buffer", "", "") if buffers: while weechat.infolist_next(buffers): buffer = weechat.infolist_pointer(buffers, "pointer") bas_signal_buffer_opened_cb("", "", buffer) weechat.infolist_free(buffers) # ==================================[ end ]=================================== def bas_unload_script(): """ Function called when script is unloaded. """ global bas_config_file if bas_config_file: bas_config_write() return weechat.WEECHAT_RC_OK weechat-scripts/python/buffer_autohide.py0000644000175100017510000003663115112622407017706 0ustar manumanu# -*- coding: utf-8 -*- # MIT License # # Copyright (c) 2017-2019 Matthias Adamczyk # Copyright (c) 2019 Marco Trevisan # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """ Automatically hide read buffers and unhide them on new activity. Requires WeeChat version 1.0 or higher. Configuration: plugins.var.python.buffer_autohide.hide_inactive: Hide inactive buffers (default: "off") plugins.var.python.buffer_autohide.hide_private: Hide private buffers (default: "off") plugins.var.python.buffer_autohide.unhide_low: Unhide a buffer when a low priority message (like JOIN, PART, etc.) has been received (default: "off"), plugins.var.python.buffer_autohide.exemptions: An enumeration of buffers that should not become hidden (default: "") plugins.var.python.buffer_autohide.keep_open: Keep a buffer open for a short amount of time (default: "off") plugins.var.python.buffer_autohide.keep_open_timeout: Timeout in milliseconds for how long a selected buffer should be kept around (default: "60 * 1000") History: 2017-03-19: Matthias Adamczyk version 0.1: Initial release 2018-06-28: yeled version 0.2: Only skip irc.servers 2018-12-07: Matthias Adamczyk version 0.3: Add a functionality to define exemptions for certain buffers 2018-12-07: Marco Trevisan version 0.4: Keep buffers active for a given time before hide them again if they should 2019-01-31: Trygve Aaberge version 0.5: Support buffers from plugins other than IRC as well https://github.com/notmatti/buffer_autohide """ from __future__ import print_function import ast import operator as op import_ok = True try: import weechat from weechat import WEECHAT_RC_OK except ImportError: print("Script must be run under weechat. https://weechat.org") import_ok = False SCRIPT_NAME = "buffer_autohide" SCRIPT_AUTHOR = "Matthias Adamczyk " SCRIPT_VERSION = "0.5" SCRIPT_LICENSE = "MIT" SCRIPT_DESC = "Automatically hide read buffers and unhide them on new activity" SCRIPT_COMMAND = SCRIPT_NAME DELIMITER = "|@|" MINIMUM_BUFFER_LIFE = 500 # How many ms are enough to consider a buffer valid KEEP_ALIVE_TIMEOUT = 60 * 1000 # How long a selected buffer should be kept around CURRENT_BUFFER = "0x0" # pointer string representation CURRENT_BUFFER_TIMER_HOOK = None # Timeout hook reference KEEP_ALIVE_BUFFERS = {} # {pointer_string_rep: timeout_hook} def config_init(): """Add configuration options to weechat.""" global KEEP_ALIVE_TIMEOUT config = { "hide_inactive": ("off", "Hide inactive buffers"), "hide_private": ("off", "Hide private buffers"), "unhide_low": ("off", "Unhide a buffer when a low priority message (like JOIN, PART, etc.) has been received"), "exemptions": ("", "An enumeration of buffers that should not get hidden"), "keep_open": ("off", "Keep a buffer open for a short amount of time"), "keep_open_timeout": ("60 * 1000", "Timeout in milliseconds for how long a selected buffer should be kept around"), } for option, default_value in config.items(): if weechat.config_get_plugin(option) == "": weechat.config_set_plugin(option, default_value[0]) weechat.config_set_desc_plugin( option, '{} (default: "{}")'.format(default_value[1], default_value[0])) weechat.hook_config("plugins.var.python.buffer_autohide.keep_open_timeout", "timeout_config_changed_cb", "") if weechat.config_is_set_plugin("keep_open_timeout"): KEEP_ALIVE_TIMEOUT = eval_expr(weechat.config_get_plugin("keep_open_timeout")) def eval_expr(expr): """Evaluate a mathematical expression. >>> eval_expr('2 * 6') 12 """ def evaluate(node): # supported operators operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor, ast.USub: op.neg} if isinstance(node, ast.Num): # return node.n elif isinstance(node, ast.BinOp): # return operators[type(node.op)](evaluate(node.left), evaluate(node.right)) elif isinstance(node, ast.UnaryOp): # e.g., -1 return operators[type(node.op)](evaluate(node.operand)) else: raise TypeError(node) return evaluate(ast.parse(expr, mode='eval').body) def timeout_config_changed_cb(data, option, value): """Set the new keep_alive timeout upon change of the corresponding value in plugins.conf.""" global KEEP_ALIVE_TIMEOUT KEEP_ALIVE_TIMEOUT = eval_expr(value) return WEECHAT_RC_OK def hotlist_dict(): """Return the contents of the hotlist as a dictionary. The returned dictionary has the following structure: >>> hotlist = { ... "0x0": { # string representation of the buffer pointer ... "count_low": 0, ... "count_message": 0, ... "count_private": 0, ... "count_highlight": 0, ... } ... } """ hotlist = {} infolist = weechat.infolist_get("hotlist", "", "") while weechat.infolist_next(infolist): buffer_pointer = weechat.infolist_pointer(infolist, "buffer_pointer") hotlist[buffer_pointer] = {} hotlist[buffer_pointer]["count_low"] = weechat.infolist_integer( infolist, "count_00") hotlist[buffer_pointer]["count_message"] = weechat.infolist_integer( infolist, "count_01") hotlist[buffer_pointer]["count_private"] = weechat.infolist_integer( infolist, "count_02") hotlist[buffer_pointer]["count_highlight"] = weechat.infolist_integer( infolist, "count_03") weechat.infolist_free(infolist) return hotlist def on_temporary_active_buffer_timeout(buffer, remaining_calls): remove_keep_alive(buffer) maybe_hide_buffer(buffer) return weechat.WEECHAT_RC_OK def keep_alive_buffer(buffer): remove_keep_alive(buffer) if buffer_is_hidable(buffer): KEEP_ALIVE_BUFFERS[buffer] = weechat.hook_timer(KEEP_ALIVE_TIMEOUT, 0, 1, "on_temporary_active_buffer_timeout", buffer) def remove_keep_alive(buffer): global KEEP_ALIVE_BUFFERS if buffer in KEEP_ALIVE_BUFFERS.keys(): weechat.unhook(KEEP_ALIVE_BUFFERS.pop(buffer)) def switch_current_buffer(): """Save current buffer and ensure that it's visible, then if the buffer is elegible to be hidden, we add it to the list of the buffers to be hidden after a delay """ global CURRENT_BUFFER global CURRENT_BUFFER_TIMER_HOOK previous_buffer = CURRENT_BUFFER CURRENT_BUFFER = weechat.current_buffer() if previous_buffer == CURRENT_BUFFER: return if weechat.buffer_get_integer(CURRENT_BUFFER, "hidden") == 1: weechat.buffer_set(CURRENT_BUFFER, "hidden", "0") if weechat.config_get_plugin("keep_open") != "off": if CURRENT_BUFFER_TIMER_HOOK is not None: weechat.unhook(CURRENT_BUFFER_TIMER_HOOK) CURRENT_BUFFER_TIMER_HOOK = None maybe_hide_buffer(previous_buffer) else: keep_alive_buffer(previous_buffer) CURRENT_BUFFER_TIMER_HOOK = weechat.hook_timer(MINIMUM_BUFFER_LIFE, 0, 1, "on_current_buffer_is_still_active_timeout", "") else: maybe_hide_buffer(previous_buffer) def on_current_buffer_is_still_active_timeout(pointer, remaining_calls): global CURRENT_BUFFER_TIMER_HOOK global KEEP_ALIVE_BUFFERS CURRENT_BUFFER_TIMER_HOOK = None remove_keep_alive(CURRENT_BUFFER) return weechat.WEECHAT_RC_OK def switch_buffer_cb(data, signal, signal_data): """ :param data: Pointer :param signal: Signal sent by Weechat :param signal_data: Data sent with signal :returns: callback return value expected by Weechat. """ switch_current_buffer() return WEECHAT_RC_OK def buffer_is_hidable(buffer): """Check if passed buffer can be hidden. If configuration option ``hide_private`` is enabled, private buffers will become hidden as well. If the previous buffer name matches any of the exemptions defined in ``exemptions``, it will not become hidden. :param buffer: Buffer string representation """ if buffer == weechat.current_buffer(): return False if buffer in KEEP_ALIVE_BUFFERS.keys(): return False full_name = weechat.buffer_get_string(buffer, "full_name") if full_name.startswith("irc.server"): return False buffer_type = weechat.buffer_get_string(buffer, 'localvar_type') if (buffer_type == "private" and weechat.config_get_plugin("hide_private") == "off"): return False if weechat.config_get_plugin("hide_inactive") == "off": nicks_count = weechat.buffer_get_integer(buffer, 'nicklist_nicks_count') if nicks_count == 0: return False for entry in list_exemptions(): if entry in full_name: return False return True def maybe_hide_buffer(buffer): """Hide a buffer if all the conditions are met""" if buffer_is_hidable(buffer): weechat.buffer_set(buffer, "hidden", "1") def unhide_buffer_cb(data, signal, signal_data): """Unhide a buffer on new activity. This callback unhides a buffer in which a new message has been received. If configuration option ``unhide_low`` is enabled, buffers with only low priority messages (like JOIN, PART, etc.) will be unhidden as well. :param data: Pointer :param signal: Signal sent by Weechat :param signal_data: Data sent with signal :returns: Callback return value expected by Weechat. """ hotlist = hotlist_dict() line_data = weechat.hdata_pointer(weechat.hdata_get('line'), signal_data, 'data') buffer = weechat.hdata_pointer(weechat.hdata_get('line_data'), line_data, 'buffer') if not buffer in hotlist.keys(): # just some background noise return WEECHAT_RC_OK if (weechat.config_get_plugin("unhide_low") == "on" and hotlist[buffer]["count_low"] > 0 or hotlist[buffer]["count_message"] > 0 or hotlist[buffer]["count_private"] > 0 or hotlist[buffer]["count_highlight"] > 0): remove_keep_alive(buffer) weechat.buffer_set(buffer, "hidden", "0") return WEECHAT_RC_OK def list_exemptions(): """Return a list of exemption defined in ``exemptions``. :returns: A list of defined exemptions. """ return [x for x in weechat.config_get_plugin("exemptions").split(DELIMITER) if x != ""] def add_to_exemptions(entry): """Add an entry to the list of exemptions. An entry can be either a #channel or server_name.#channel :param entry: The entry to add. :returns: the new list of entries. The return value is only used for unit testing. """ entries = list_exemptions() entries.append(entry) weechat.config_set_plugin("exemptions", DELIMITER.join(entries)) weechat.prnt("", "[{}] add: {} added to exemptions.".format(SCRIPT_COMMAND, entry)) return entries def del_from_exemptions(entry): """Remove an entry from the list of defined exemptions. :param entry: The entry to delete, which can be specified by the position in the list or by the name itself. :returns: the new list of entries. The return value is only used for unit testing. """ entries = list_exemptions() try: # by index try: index = int(entry) - 1 if index < 0: raise IndexError entry = entries.pop(index) except IndexError: weechat.prnt("", "[{}] del: Index out of range".format(SCRIPT_COMMAND)) return entries except ValueError: try: # by name entries.remove(entry) weechat.config_set_plugin("exemptions", DELIMITER.join(entries)) except ValueError: weechat.prnt("", "[{}] del: Could not find {}".format(SCRIPT_COMMAND, entry)) return entries weechat.config_set_plugin("exemptions", DELIMITER.join(entries)) weechat.prnt("", "[{}] del: Removed {} from exemptions.".format(SCRIPT_COMMAND, entry)) return entries def print_exemptions(): """Print all exemptions defined in ``exemptions``""" entries = list_exemptions() if entries: count = 1 for entry in entries: weechat.prnt("", "[{}] {}: {}".format(SCRIPT_COMMAND, count, entry)) count += 1 else: weechat.prnt("", "[{}] list: No exemptions defined so far.".format(SCRIPT_COMMAND)) def command_cb(data, buffer, args): """Weechat callback for parsing and executing the given command. :returns: Callback return value expected by Weechat. """ list_args = args.split(" ") if list_args[0] not in ["add", "del", "list"]: weechat.prnt("", "[{0}] bad option while using /{0} command, try '/help {0}' for more info".format( SCRIPT_COMMAND)) elif list_args[0] == "add": if len(list_args) == 2: add_to_exemptions(list_args[1]) elif list_args[0] == "del": if len(list_args) == 2: del_from_exemptions(list_args[1]) elif list_args[0] == "list": print_exemptions() else: weechat.command("", "/help " + SCRIPT_COMMAND) return WEECHAT_RC_OK if (__name__ == '__main__' and import_ok and weechat.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', '')): weechat_version = weechat.info_get("version_number", "") or 0 if int(weechat_version) >= 0x01000000: config_init() CURRENT_BUFFER = weechat.current_buffer() weechat.hook_signal("buffer_switch", "switch_buffer_cb", "") weechat.hook_signal("buffer_line_added", "unhide_buffer_cb", "") weechat.hook_command( SCRIPT_NAME, SCRIPT_DESC, "add $buffer_name | del { $buffer_name | $list_position } | list", " add : Add $buffer_name to the list of exemptions\n" " $buffer_name can be either #channel or server_name.#channel\n" " del : Delete $buffer_name from the list of exemptions\n" " list : Return a list of all buffers that should not become hidden.", "add|del|list", "command_cb", "" ) else: weechat.prnt("", "{}{} requires WeeChat version 1.0 or higher".format( weechat.prefix('error'), SCRIPT_NAME)) weechat-scripts/python/buffer_bind.py0000644000175100017510000000410215112622406017003 0ustar manumanu# -*- coding: utf-8 -*- # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import weechat as w SCRIPT_NAME = "buffer_bind" SCRIPT_AUTHOR = "Trevor 'tee' Slocum " SCRIPT_VERSION = "1.0" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Bind meta- to the current buffer" SCRIPT_NOTE = """Case sensitivity is controlled via plugins.var.python.%s.case_sensitive (default: off) %s is a port of irssi's window_alias written by veli@piipiip.net""" % (SCRIPT_NAME, SCRIPT_NAME) SETTINGS = { "case_sensitive": "off" } def command_buffer_bind(data, buffer, args): if len(args) == 1 and args[0] != "": bindkey = args[0] buffername = w.buffer_get_string(buffer, "name") bind_keys = [bindkey] if w.config_get_plugin("case_sensitive") == "off" and bindkey.isalpha(): bind_keys.append(bindkey.swapcase()) for bind_keys_i in bind_keys: w.command(buffer, "/key bind meta-%s /buffer %s" % (bind_keys_i, buffername)) w.prnt(buffer, "Buffer %s is now accessible with meta-%s" % (buffername, bindkey)) else: w.command(buffer, "/help %s" % SCRIPT_NAME) return w.WEECHAT_RC_OK_EAT if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""): for option, value in SETTINGS.items(): if not w.config_is_set_plugin(option): w.config_set_plugin(option, value) w.hook_command(SCRIPT_NAME, SCRIPT_DESC, "", SCRIPT_NOTE, "key", "command_buffer_bind", "") weechat-scripts/python/completion.py0000644000175100017510000001677615112622377016742 0ustar manumanu# -*- coding: utf-8 -*- ### # Copyright (c) 2010 by Elián Hanisch # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ### ### # # This scripts adds word completion, like irssi's /completion # (depends on WeeChat 0.3.1 or newer) # # Commands: # * /completion: see /help completion # # # Settings: # * plugins.var.python.completion.replace_values: # Completion list, it shouldn't be edited by hand. # # # History: # 2019-08-20 # version 0.3: Ben Harris (benharri) # * port for python3 # # 2010-05-08 # version 0.2: # * complete any word behind the cursor, not just the last one in input line. # * change script display name 'completion' to 'cmpl'. # # 2010-01-26 # version 0.1: release # ### try: import weechat WEECHAT_RC_OK = weechat.WEECHAT_RC_OK import_ok = True except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: http://www.weechat.org/") import_ok = False SCRIPT_NAME = "completion" SCRIPT_AUTHOR = "Elián Hanisch " SCRIPT_VERSION = "0.3" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Word completions for WeeChat" SCRIPT_COMMAND = "completion" ### Config ### settings = { 'replace_values':'' } ### Messages ### def debug(s, prefix='', buffer=None): """Debug msg""" #if not weechat.config_get_plugin('debug'): return if buffer is None: buffer_name = 'DEBUG_' + SCRIPT_NAME buffer = weechat.buffer_search('python', buffer_name) if not buffer: buffer = weechat.buffer_new(buffer_name, '', '', '', '') weechat.buffer_set(buffer, 'nicklist', '0') weechat.buffer_set(buffer, 'time_for_each_line', '0') weechat.buffer_set(buffer, 'localvar_set_no_log', '1') weechat.prnt(buffer, '%s\t%s' %(prefix, s)) def error(s, prefix=None, buffer='', trace=''): """Error msg""" prefix = prefix or script_nick weechat.prnt(buffer, '%s%s %s' %(weechat.prefix('error'), prefix, s)) if weechat.config_get_plugin('debug'): if not trace: import traceback if traceback.sys.exc_type: trace = traceback.format_exc() not trace or weechat.prnt('', trace) def say(s, prefix=None, buffer=''): """normal msg""" prefix = prefix or script_nick weechat.prnt(buffer, '%s\t%s' %(prefix, s)) print_replace = lambda k,v : say('%s %s=>%s %s' %(k, color_delimiter, color_reset, v)) ### Config functions ### def get_config_dict(config): value = weechat.config_get_plugin(config) if not value: return {} values = [s.split('=>') for s in value.split(';;')] #debug(values) return dict(values) def load_replace_table(): global replace_table replace_table = dict(get_config_dict('replace_values')) def save_replace_table(): global replace_table weechat.config_set_plugin('replace_values', ';;'.join(['%s=>%s' %(k, v) for k, v in replace_table.items()])) ### Commands ### def cmd_completion(data, buffer, args): global replace_table if not args: if replace_table: for k, v in replace_table.items(): print_replace(k, v) else: say('No completions.') return WEECHAT_RC_OK cmd, space, args = args.partition(' ') if cmd == 'add': word, space, text = args.partition(' ') k, v = word.strip(), text.strip() replace_table[k] = v save_replace_table() say('added: %s %s=>%s %s' %(k, color_delimiter, color_reset, v)) elif cmd == 'del': k = args.strip() try: del replace_table[k] save_replace_table() say("completion for '%s' deleted." %k) save_replace_table() except KeyError: error("completion for '%s' not found." %k) return WEECHAT_RC_OK ### Completion ### def completion_replacer(data, completion_item, buffer, completion): global replace_table pos = weechat.buffer_get_integer(buffer, 'input_pos') input = weechat.buffer_get_string(buffer, 'input') #debug('%r %s %s' %(input, len(input), pos)) if pos > 0 and (pos == len(input) or input[pos] == ' '): n = input.rfind(' ', 0, pos) word = input[n+1:pos] #debug(repr(word)) if word in replace_table: replace = replace_table[word] if pos >= len(input.strip()): # cursor is in the end of line, append a space replace += ' ' n = len(word) input = '%s%s%s' %(input[:pos-n], replace, input[pos:]) weechat.buffer_set(buffer, 'input', input) weechat.buffer_set(buffer, 'input_pos', str(pos - n + len(replace))) return WEECHAT_RC_OK def completion_keys(data, completion_item, buffer, completion): global replace_table for k in replace_table: weechat.hook_completion_list_add(completion, k, 0, weechat.WEECHAT_LIST_POS_SORT) return WEECHAT_RC_OK ### Main ### if __name__ == '__main__' and import_ok and \ weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, \ SCRIPT_DESC, '', ''): # colors color_delimiter = weechat.color('chat_delimiters') color_script_nick = weechat.color('chat_nick') color_reset = weechat.color('reset') # pretty [SCRIPT_NAME] script_nick = '%s[%s%s%s]%s' %(color_delimiter, color_script_nick, 'cmpl', color_delimiter, color_reset) version = weechat.info_get('version', '') if version == '0.3.0': error('WeeChat 0.3.1 or newer is required for this script.') else: # settings for opt, val in settings.items(): if not weechat.config_is_set_plugin(opt): weechat.config_set_plugin(opt, val) load_replace_table() completion_template = 'completion_script' weechat.hook_completion(completion_template, "Replaces last word in input by its configured value.", 'completion_replacer', '') weechat.hook_completion('completion_keys', "Words in completion list.", 'completion_keys', '') weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC , "[add |del ]", """ add: adds a new completion, => . del: deletes a completion. Without arguments it displays current completions. will be replaced by when pressing tab in input line, where is any word currently behind the cursor. Setup: For this script to work, you must add the template %%(%(completion)s) to the default completion template, use: /set weechat.completion.default_template "%%(nicks)|%%(irc_channels)|%%(%(completion)s)" Examples: /%(command)s add wee WeeChat (typing wee will replace 'wee' by 'WeeChat') /%(command)s add weeurl http://www.weechat.org/ /%(command)s add test This is a test! """ %dict(completion=completion_template, command=SCRIPT_COMMAND), 'add|del %(completion_keys)', 'cmd_completion', '') # vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: weechat-scripts/python/undernet_totp.py0000644000175100017510000003553515112622404017444 0ustar manumanu# -*- coding: utf-8 -*- # # Copyright (C) 2013 - 2019 Stefan Wold # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # (This script requires WeeChat 0.4.2 or higher). # # WeeChat script that enables automatic OTP (OATH-TOTP) support for UnderNET's X # and Login on Connect (LoC) authentication. # # The script will generate an OTP and automatically append it when it # notices /msg x@channels.undernet.org login command. # This allows OTP login when using irc.server.*.command to automatically # sign in to the X service when connecting to an undernet server. # # Commands: # /uotp otp [server] # /uotp list # /uotp add # /uotp remove # /uotp enable # /uotp disable from __future__ import print_function SCRIPT_NAME = "undernet_totp" SCRIPT_AUTHOR = "Stefan Wold " SCRIPT_VERSION = "0.4.2" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Automatic OTP (OATH-TOTP) authentication with UnderNET's channel services (X) and Login on Connect (LoC)." SCRIPT_COMMAND = "uotp" HOOKS = {} SETTINGS = { "otp_server_names": ("", "List of undernet server for which to enable OTP, use comma as separator"), "debug" : ("off", "Debug output"), } import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") import_ok = False try: import sys import hmac import re from base64 import b32decode from hashlib import sha1 from struct import pack, unpack from time import time from binascii import unhexlify except ImportError as err: print("Missing module(s) for %s: %s" % (SCRIPT_NAME, err)) import_ok = False def print_debug(message): if weechat.config_get_plugin('debug') == 'on': weechat.prnt("", "%s DEBUG: %s" % (SCRIPT_NAME, message)) def sprint(message, buffer=""): weechat.prnt(buffer, "%s: %s" % (SCRIPT_NAME, message)) def unhook(hook): global HOOKS if hook in HOOKS: print_debug('Unhooking %s' % hook) weechat.unhook(HOOKS[hook]) del HOOKS[hook] def unhook_all(server): for hook in [server+'.notice', server+'.modifier', server+'.modifier2']: unhook(hook) def hook_all(server): print_debug("hook_all(%s)" % server) global HOOKS notice = server + '.notice' modifier = server + '.modifier' modifier2 = server + '.modifier2' if notice not in HOOKS: HOOKS[notice] = weechat.hook_signal("%s,irc_raw_in_notice" % server, "auth_success_cb", server) if modifier not in HOOKS: HOOKS[modifier] = weechat.hook_modifier("irc_out_privmsg", "totp_login_modifier_cb", server) if modifier2 not in HOOKS: HOOKS[modifier2] = weechat.hook_modifier("irc_out_pass", "totp_login_modifier_cb", server) def totp_login_modifier_cb(data, modifier, server, cmd): if server == data and server in enabled_servers(): if re.match(r'(?i)^(PRIVMSG x@channels.undernet.org :login .+ .+|PASS .*)', cmd): print_debug("totp_login_modifier_cb(%s)" % cmd) otp = generate_totp(server) if otp is not None: cmd += " %s" % otp return cmd def auth_success_cb(server, signal, signal_data): if signal_data.startswith(":X!cservice@undernet.org NOTICE"): if re.match(r'^:X!cservice@undernet.org NOTICE .+ :AUTHENTICATION SUCCESSFUL', signal_data): unhook_all(server) return weechat.WEECHAT_RC_OK def signal_cb(data, signal, server): if server in enabled_servers(): print_debug('signal_cb(%s)' % signal) if signal == 'irc_server_connecting': hook_all(server) elif signal == 'irc_server_disconnected': unhook_all(server) return weechat.WEECHAT_RC_OK def get_otp_cb(data, buffer, server): if server: server = [server] else: server = enabled_servers() for _server in server: otp = generate_totp(_server) if otp is not None: weechat.prnt("", "%s OTP: %s" % (_server, otp)) return weechat.WEECHAT_RC_OK def get_irc_servers(): """ Returns a list of configured IRC servers in weechat""" serverptrlist = weechat.infolist_get('irc_server', '', '') serverlist = [] while weechat.infolist_next(serverptrlist): serverlist.append(weechat.infolist_string(serverptrlist, 'name')) weechat.infolist_free(serverptrlist) return serverlist def enabled_servers(): """ Return a list of TOTP enabled servers. """ serverlist = get_irc_servers() return [s for s in get_config_as_list('otp_server_names') if s in serverlist] def disabled_servers(): """ Return a list of configured TOTP servers that are currently disabled. """ serverlist = get_irc_servers() server_seed_list = [server for server in serverlist if weechat.string_eval_expression("${sec.data.%s_seed}" % server, {}, {}, {}) and server not in get_config_as_list('otp_server_names')] return [s for s in server_seed_list if s in serverlist] def configured_servers(): """ Return a lost of servers with an existing seed. """ serverlist = get_irc_servers() return [s for s in serverlist if weechat.string_eval_expression("${sec.data.%s_seed}" % s, {}, {}, {})] def generate_totp(server, period=30, buffer=""): print_debug('generate_totp(%s)' % server) seed = weechat.string_eval_expression("${sec.data.%s_seed}" % server, {}, {}, {}) if not seed: sprint("No OATH-TOTP secret set, use: /uotp add %s " % server, buffer) return None if len(seed) == 40: # Assume hex format seed = unhexlify(seed) else: seed = b32decode(seed.replace(" ", ""), True) def _ord(b): if sys.version_info[0] < 3 or type(b) == str: return ord(b) return b t = pack(">Q", int(time() / period)) _hmac = hmac.new(seed, t, sha1).digest() o = _ord(_hmac[19]) & 15 otp = (unpack(">I", _hmac[o:o+4])[0] & 0x7fffffff) % 1000000 return '%06d' % otp def config_update_cb(data, option, value): """ Reload hooks on configuration change. """ print_debug("config_cb(%s)" % value) [hook_all(s.strip()) for s in value.split(',')] return weechat.WEECHAT_RC_OK def options_cb(data, buffer, args): """ Script configuration callback """ if not args: weechat.command("", "/help %s" % SCRIPT_COMMAND) args = args.strip().split(' ') opt = args[0] opt_args = args[1:] if opt == 'otp': if opt_args: servers = [opt_args[0]] else: servers = enabled_servers() for server in servers: otp = generate_totp(server, buffer=buffer) if otp: sprint("%s = %s" % (server, otp), buffer) elif opt == 'list': sprint("List of configured servers", buffer) for server in enabled_servers(): weechat.prnt(buffer, " - %s [enabled]" % server) for server in disabled_servers(): weechat.prnt(buffer, " - %s [disabled]" % server) elif opt == 'add': if len(opt_args) >= 2: if opt_args[0] not in enabled_servers() and opt_args[0] in get_irc_servers(): #weechat.command("", "/secure set %s_seed %s" % (opt_args[0], opt_args[1])) try: add_server(opt_args[0], opt_args[1:]) sprint("server '%s' was successfully added" % opt_args[0], buffer) except Exception as ex: sprint("invalid TOTP seed provided", buffer) elif opt_args[0] not in get_irc_servers(): sprint("No server named '%s' was found, see /help server" % opt_args[0], buffer) else: sprint("OTP already configured for '%s', to change remove the existing one first." % opt_args[0], buffer) else: sprint("/uotp -- invalid argument, valid command is /uotp add ", buffer) elif opt == 'remove': if opt_args[0] in enabled_servers() or opt_args[0] in disabled_servers(): remove_server(opt_args[0], True) sprint("server '%s' was successfully removed" % opt_args[0], buffer) else: sprint("failed to remove server, '%s' not found" % opt_args[0], buffer) elif opt == 'enable': if opt_args and opt_args[0] not in enabled_servers(): if opt_args[0] in get_irc_servers(): add_server(opt_args[0]) sprint("server '%s' was successfully enabled" % opt_args[0], buffer) else: sprint("No server named '%s' was found, see /help server" % opt_args[0], buffer) else: sprint("OTP is already enabled for the server '%s'." % opt_args[0], buffer) elif opt == 'disable': if opt_args and opt_args[0] in enabled_servers(): remove_server(opt_args[0]) else: sprint("OTP does not seem to be enabled for '%s'" % opt_args[0], buffer) elif opt: sprint("/uotp: invalid option -- '%s'" % opt, buffer) weechat.command("", "/help %s" % SCRIPT_COMMAND) return weechat.WEECHAT_RC_OK def get_config_as_list(option): """ Return comma-separated