+++ /dev/null
-# -*- coding: utf-8 -*-
-###
-# Copyright (c) 2009-2010 by Elián Hanisch <lambdae2@gmail.com>
-#
-# 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 <http://www.gnu.org/licenses/>.
-###
-
-###
-# Notifications for WeeChat
-#
-# Notification script that uses libnotify or dbus, supports WeeChat inside screen.
-# Uses a xmlrpc daemon that must be running in the receiving machine (remotely or locally)
-#
-# The daemon can be setup in several ways, see inotify-daemon --help
-# Download it from 'http://github.com/m4v/inotify-daemon/raw/stable/inotify-daemon'
-#
-# Commands:
-# * /inotify
-# See /help inotify
-#
-# Settings:
-# * plugins.var.python.inotify.server_uri:
-# inotify-daemon address and port to connect, must be the same address the daemon is using.
-# By default it uses localhost and port 7766.
-#
-# Examples:
-# http://www.your.home.com:7766
-# http://localhost:7766
-#
-# * plugins.var.python.inotify.server_method:
-# Notification method supported by the daemon to use. Defaults to 'libnotify'.
-# See below for detailed help about them.
-#
-# * plugins.var.python.inotify.color_nick:
-# Will use coloured nicks in notifications.
-#
-# * plugins.var.python.inotify.ignore_channel:
-# Comma separated list of patterns for define ignores. Notifications from channels where its name
-# matches any of these patterns will be ignored.
-# Wildcards '*', '?' and char groups [..] can be used.
-# An ignore exception can be added by prefixing '!' in the pattern.
-#
-# Example:
-# *ubuntu*,!#ubuntu-offtopic
-# any notifications from a 'ubuntu' channel will be ignored, except from #ubuntu-offtopic
-#
-# * plugins.var.python.inotify.ignore_nick:
-# Same as ignore_channel, but for nicknames.
-#
-# Example:
-# troll,b[0o]t
-# will ignore notifications from troll, bot and b0t
-#
-# * plugins.var.python.inotify.ignore_text:
-# Same as ignore_channel, but for the contents of the message.
-#
-# * plugins.var.python.inotify.passwd:
-# In the case the daemon is using a password for verify that incoming notifications are trusted.
-# If this password doesn't match with the password setup in inotify-daemon notification will not
-# succeed.
-#
-#
-# Notify methods:
-# * libnotify:
-# Use libnotify for notifications, needs python-notify installed in the machine running the
-# daemon. This is the default method.
-#
-# * dbus:
-# Uses dbus directly for notifications, this is KDE4 specific, might not work in other desktops.
-# Needs python-dbus in the machine running the daemon.
-#
-# * any:
-# Use daemon's configured method, this is usually libnotify.
-#
-#
-# TODO
-# add commands for configure ignores
-# add more notifications methods (?)
-#
-#
-# History:
-# 2014-05-10, Sébastien Helleu <flashcode@flashtux.org>
-# version 0.1.4: change hook_print callback argument type of
-# displayed/highlight (WeeChat >= 1.0)
-# 2011-11-02, Sebastien Helleu <flashcode@flashtux.org>:
-# version 0.1.3: use local variable "channel" in buffer instead of reading "short_name",
-# fix command for hook_process (remove line break before "-c")
-#
-# 2011-03-11, Sebastien Helleu <flashcode@flashtux.org>:
-# version 0.1.2: get python 2.x binary for hook_process (fix problem when
-# python 3.x is default python version)
-# 2010-03-10:
-# version 0.1.1: fixes
-# * improved shell escapes when using hook_process
-# * fix ACTION messages
-#
-# 2010-02-24
-# version 0.1: release!
-#
-###
-
-SCRIPT_NAME = "inotify"
-SCRIPT_AUTHOR = "Elián Hanisch <lambdae2@gmail.com>"
-SCRIPT_VERSION = "0.1.4"
-SCRIPT_LICENSE = "GPL3"
-SCRIPT_DESC = "Notifications for WeeChat."
-SCRIPT_COMMAND = "inotify"
-
-DAEMON_URL = 'http://github.com/m4v/inotify-daemon/raw/stable/inotify-daemon'
-DAEMON = 'inotify-daemon'
-DAEMON_VERSION = '0.2'
-
-### Default Settings ###
-settings = {
-'server_uri' : 'http://localhost:7766',
-'server_method' : 'any',
-'color_nick' : 'on',
-'ignore_channel' : '',
-'ignore_nick' : '',
-'ignore_text' : '',
-'passwd' : '',
-}
-
-max_error_count = 3
-
-try:
- import weechat
- WEECHAT_RC_OK = weechat.WEECHAT_RC_OK
- import_ok = True
-except:
- print "This script must be run under WeeChat."
- print "Get WeeChat now at: http://www.weechat.org/"
- import_ok = False
-
-import xmlrpclib, socket
-from fnmatch import fnmatch
-
-# remote daemon timeout
-socket.setdefaulttimeout(4)
-
-### Messages ###
-def debug(s, prefix=''):
- """Debug msg"""
- if not weechat.config_get_plugin('debug'): return
- 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='', buffer='', trace=''):
- """Error msg"""
- if weechat.config_get_plugin('quiet'): return
- 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='', buffer=''):
- """normal msg"""
- prefix = prefix or script_nick
- weechat.prnt(buffer, '%s\t%s' %(prefix, s))
-
-### Config and value validation ###
-boolDict = {'on':True, 'off':False}
-def get_config_boolean(config):
- value = weechat.config_get_plugin(config)
- try:
- return boolDict[value]
- except KeyError:
- default = settings[config]
- error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
- error("'%s' is invalid, allowed: 'on', 'off'" %value)
- return boolDict[default]
-
-def get_config_int(config, allow_empty_string=False):
- value = weechat.config_get_plugin(config)
- try:
- return int(value)
- except ValueError:
- if value == '' and allow_empty_string:
- return value
- default = settings[config]
- error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
- error("'%s' is not a number." %value)
- return int(default)
-
-valid_methods = set(('any', 'dbus', 'libnotify'))
-def get_config_valid_string(config, valid_strings=valid_methods):
- value = weechat.config_get_plugin(config)
- if value not in valid_strings:
- default = settings[config]
- error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
- error("'%s' is an invalid value, allowed: %s." %(value, ', '.join(valid_strings)))
- return default
- return value
-
-### Class definitions ###
-class Ignores(object):
- def __init__(self, ignore_type):
- self.ignore_type = ignore_type
- self.ignores = []
- self.exceptions = []
- self._get_ignores()
-
- def _get_ignores(self):
- assert self.ignore_type is not None
- ignores = weechat.config_get_plugin(self.ignore_type).split(',')
- ignores = [ s.lower() for s in ignores if s ]
- self.ignores = [ s for s in ignores if s[0] != '!' ]
- self.exceptions = [ s[1:] for s in ignores if s[0] == '!' ]
-
- def __contains__(self, s):
- s = s.lower()
- for p in self.ignores:
- if fnmatch(s, p):
- for e in self.exceptions:
- if fnmatch(s, e):
- return False
- return True
- return False
-
-
-class Server(object):
- def catch_exceptions(f):
- """
- This decorator is for catch exceptions in methods that communicate with the daemon."""
- def protected_method(self, *args):
- try:
- return f(self, *args)
- except xmlrpclib.Fault, e:
- self._error("A fault occurred." %e, trace="Code: %s\nString: %s" %(e.faultCode, e.faultString))
- except xmlrpclib.ProtocolError, e:
- self._error("Protocol error: %s" %e, trace="Url: %s\nCode: %s\nHeaders: %s\nMessage: %s" %(e.url, e.errcode,
- e.headers, e.errmsg))
- except socket.error, e:
- self._error_connect()
- except socket.timeout, e:
- self._error('Timeout while sending to our daemon.')
- if self.error_count < 3: # don't re-queue after 3 errors
- return 'retry'
- except:
- # catch all exception
- self._error("An error occurred, but I'm not sure what could it be...")
- return protected_method
-
- def __init__(self):
- self._reset()
- self._create_server()
- if not self.error_count:
- self.send_rpc('Notification script loaded')
-
- def _reset(self):
- self.msg = {}
- self.timer = None
-
- def enqueue(self, msg, channel):
- self._enqueue(msg, channel)
-
- def _enqueue(self, msg, channel='', timeout=3000):
- if channel not in self.msg:
- self.msg[channel] = msg
- else:
- s = self.msg[channel]
- msg = '%s\n%s' %(s, msg)
- self.msg[channel] = msg
- if self.timer is None:
- self.timer = weechat.hook_timer(timeout, 0, 1, 'msg_flush', '')
- #debug('set timer: %s %s' %(self.timer, timeout))
-
- def flush(self):
- for channel, msg in self.msg.iteritems():
- if self.send_rpc(msg, channel) == 'retry':
- # daemon is restarting, try again later
- self._restart_timer()
- return
- if self.remote:
- # we can't stop flushing if we're in remote mode, so save a copy as we might need
- # to repeat the queue later
- self.msg_bak = self.msg.copy()
- self._reset()
-
- def _restart_timer(self):
- if self.timer is not None:
- #debug('reset and set timer')
- weechat.unhook(self.timer)
- self.timer = weechat.hook_timer(5000, 0, 1, 'msg_flush', '')
-
- @catch_exceptions
- def _create_server(self):
- self.error_count = 0
- self.method = get_config_valid_string('server_method')
- self.address = weechat.config_get_plugin('server_uri')
- # detect if we're going to connect to localhost.
- if self.address[:17] in ('http://localhost:', 'http://127.0.0.1:'):
- self.remote = False
- else:
- self.remote = True
- self.msg_bak = {}
- self.server = xmlrpclib.Server(self.address)
- version = self.server.version()
- if version != DAEMON_VERSION:
- error('Incorrect daemon version, should be %s, but got %s' %(DAEMON_VERSION,
- version))
- error('Download the latest %s from %s' %(DAEMON, DAEMON_URL))
-
- def _error(self, s, **kwargs):
- if self.error_count < max_error_count: # stop sending error msg after max reached
- error(s, **kwargs)
- elif self.error_count == max_error_count:
- error('Suppressing future error messages...')
- self.error_count += 1
-
- def _error_connect(self):
- self._error('Failed to connect to our notification daemon, check if the address'
- ' \'%s\' is correct and if it\'s running.' %self.address)
-
- @catch_exceptions
- def send_rpc(self, *args):
- debug('sending rpc: %s' %' '.join(map(repr, args)))
- passwd = weechat.config_get_plugin('passwd')
- if self.remote:
- return self._send_rpc_process(passwd, *args)
- rt = getattr(self.server, self.method)(passwd, *args)
- if rt == 'OK':
- self.error_count = 0
- #debug('Success: %s' % rt)
- elif rt.startswith('warning:'):
- self._error(rt[8:])
- if self.error_count < 10: # don't re-queue after 10 errors
- #debug('repeating queue')
- # returning 'retry' will cause flush() to try to send msgs again later
- return 'retry'
- else:
- error(rt)
-
- def _send_rpc_process(self, *args):
- def quoted(s):
- """
- Is important to escape quotes properly so hook_process doesn't break or sends stuff
- outside single quotes."""
- if '\\' in s:
- # escape any backslashes
- s = s.replace('\\', '\\\\')
- if '"' in s:
- s = s.replace('"', '\\"')
- if "'" in s:
- # I must escape single quotes with \'\\\'\' because they will be within single
- # quotes in the command string. Awesome.
- s = s.replace("'", "'\\''")
- return '"""%s"""' %s
-
- args = ', '.join(map(quoted, args))
- python2_bin = weechat.info_get('python2_bin', '') or 'python'
- cmd = python2_bin + rpc_process_cmd %{'server_uri':self.address, 'method':self.method, 'args':args}
- debug('\nRemote cmd:%s\n' %cmd)
- weechat.hook_process(cmd, 30000, 'rpc_process_cb', '')
-
- @catch_exceptions
- def quit(self):
- passwd = weechat.config_get_plugin('passwd')
- rt = self.server.quit(passwd)
- debug(rt)
- if rt != 'OK':
- error(rt)
-
- @catch_exceptions
- def restart(self):
- passwd = weechat.config_get_plugin('passwd')
- rt = self.server.restart(passwd)
- debug(rt)
- if rt != 'OK':
- error(rt)
-
-
-### Functions ###
-def msg_flush(*args):
- server.flush()
- return WEECHAT_RC_OK
-
-# command MUST be within single quotes, otherwise the shell would try to expand stuff and it might
-# be real nasty, somebody could run arbitrary code with a highlight.
-rpc_process_cmd = """ -c '
-import xmlrpclib
-try:
- server = xmlrpclib.Server("%(server_uri)s")
- print getattr(server, "%(method)s")(%(args)s)
-except Exception, e:
- print "error: %%s" %%e'
-"""
-
-def rpc_process_cb(data, command, rc, stdout, stderr):
- #debug("%s\nstderr: %s\nstdout: %s" %(rc, repr(stderr), repr(stdout)))
- if stdout:
- debug('Reply: %s' %stdout)
- if stdout == 'OK\n':
- server.error_count = 0
- elif stdout.startswith('warning:'):
- server._error(stdout[8:])
- if server.error_count < 10:
- server.msg = server.msg_bak
- server._restart_timer()
- else:
- server._error(stdout)
- if stderr:
- error(stderr)
- return WEECHAT_RC_OK
-
-color_table = ('teal', 'darkmagenta', 'darkgreen', 'brown', 'blue', 'darkblue', 'darkcyan', 'magenta', 'green', 'grey')
-
-def color_tag(nick):
- n = len(color_table)
- #generic_nick = nick.strip('_`').lower()
- id = (sum(map(ord, nick))%n)
- #debug('%s:%s' %(nick, id))
- return '<font color=%s><%s></font>' %(color_table[id], nick)
-
-def format(s, nick=''):
- if '<' in s:
- s = s.replace('<', '<')
- if '>' in s:
- s = s.replace('>', '>')
- if '"' in s:
- s = s.replace('"', '"')
- if '\n' in s:
- s = s.replace('\n', '<br/>')
- if nick:
- if get_config_boolean('color_nick'):
- nick = color_tag(nick)
- else:
- nick = '<%s>' %nick
- s = '<b>%s</b> %s' %(nick, s)
- return s
-
-def send_notify(s, channel='', nick=''):
- #command = getattr(server, 'kde4')
- s = format(s, nick)
- server.enqueue(s, channel)
-
-def is_displayed(buffer):
- """Returns True if buffer is in a window and the user is active. This is for not show
- notifications of a visible buffer while the user is doing something and wouldn't need to be
- notified."""
- window = weechat.buffer_get_integer(buffer, 'num_displayed')
- if window != 0:
- return not inactive()
- return False
-
-def inactive():
- inactivity = int(weechat.info_get('inactivity', ''))
- #debug('user inactivity: %s' %inactivity)
- if inactivity > 20:
- return True
- else:
- return False
-
-config_string = lambda s : weechat.config_string(weechat.config_get(s))
-def get_nick(s):
- """Strip nickmodes and prefix, suffix."""
- if not s: return ''
- # prefix and suffix
- prefix = config_string('irc.look.nick_prefix')
- suffix = config_string('irc.look.nick_suffix')
- if s[0] == prefix:
- s = s[1:]
- if s[-1] == suffix:
- s = s[:-1]
- # nick mode
- modes = '~+@!%'
- s = s.lstrip(modes)
- return s
-
-def notify_msg(workaround, buffer, time, tags, display, hilight, prefix, msg):
- if workaround and 'notify_message' not in tags and 'notify_private' not in tags:
- # weechat 0.3.0 bug
- return WEECHAT_RC_OK
- #debug(' '.join((buffer, time, tags, display, hilight, prefix, 'msg_len:%s' %len(msg))),
- # prefix='MESSAGE')
- private = 'notify_private' in tags
- if (int(hilight) or private) and int(display):
- if 'irc_action' in tags:
- prefix, _, msg = msg.partition(' ')
- msg = '%s %s' %(config_string('weechat.look.prefix_action'), msg)
- prefix = get_nick(prefix)
- if prefix not in ignore_nick \
- and msg not in ignore_text \
- and not is_displayed(buffer):
- #debug('%sSending notification: %s' %(weechat.color('lightgreen'), channel), prefix='NOTIFY')
- if not private:
- channel = weechat.buffer_get_string(buffer, 'localvar_channel')
- if channel not in ignore_channel:
- send_notify(msg, channel=channel, nick=prefix)
- else:
- send_notify(msg, channel=prefix)
- return WEECHAT_RC_OK
-
-def cmd_notify(data, buffer, args):
- if args:
- args = args.split()
- cmd = args[0]
- if cmd in ('test', 'quit', 'restart', 'notify'):
- if cmd == 'test':
- server.send_rpc(' '.join(args[1:]) or 'This is a test.', '#test')
- elif cmd == 'notify':
- send_notify(' '.join(args[1:]) or 'This is a test.', '#test')
- elif cmd == 'quit':
- say('Shutting down notification daemon...')
- server.quit()
- elif cmd == 'restart':
- say('Restarting notification daemon...')
- server.restart()
- return WEECHAT_RC_OK
-
- weechat.command('', '/help %s' %SCRIPT_COMMAND)
- return WEECHAT_RC_OK
-
-def ignore_update(*args):
- ignore_channel._get_ignores()
- ignore_nick._get_ignores()
- ignore_text._get_ignores()
- return WEECHAT_RC_OK
-
-def server_update(*args):
- server._create_server()
- return WEECHAT_RC_OK
-
-
-if __name__ == '__main__' and import_ok and \
- weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC,
- '', ''):
-
- # pretty nick
- color_delimiter = weechat.color('chat_delimiters')
- color_nick = weechat.color('chat_nick')
- color_reset = weechat.color('reset')
- script_nick = '%s[%s%s%s]%s' %(color_delimiter, color_nick, SCRIPT_NAME, color_delimiter, color_reset)
-
- # check if we need to workaround a bug in 0.3.0
- workaround = ''
- version = weechat.info_get('version', '')
- if version == '0.3.0':
- workaround = '1'
- #debug('workaround enabled')
-
- for opt, val in settings.iteritems():
- if not weechat.config_is_set_plugin(opt):
- weechat.config_set_plugin(opt, val)
-
- ignore_channel = Ignores('ignore_channel')
- ignore_nick = Ignores('ignore_nick')
- ignore_text = Ignores('ignore_text')
-
- server = Server()
-
- weechat.hook_command(SCRIPT_COMMAND, SCRIPT_DESC, '[test [text] | notify [text] | restart | quit ]',
-"""\
- test: sends a test notification, with 'text' if provided ('text'
- is sent raw).
- notify: same as test, but the notification is sent through the
- notification queue and after formatting.
-restart: forces remote daemon to restart.
- quit: forces remote daemon to shutdown, after this notifications
- won't be available and the daemon should be started again
- manually.
-
-Setting notification ignores:
- It's possible to filter notification by channel, by nick or by
- message content, with the config options ignore_channel,
- ignore_nick and ignore_text in plugins.var.python.%(script)s
- Each config option accepts a comma separated list of patterns.
- Wildcards '*', '?' and char groups [..] can be used.
- An ignore exception can be added by prefixing '!' in the pattern.
-
-Examples:
- Setting 'ignore_nick' to 'troll,b[0o]t':
- will ignore notifications from troll, bot and b0t.
- Setting 'ignore_channel' to '*ubuntu*,!#ubuntu-offtopic':
- will ignore notifications from any channel with the word 'ubuntu'
- except from #ubuntu-offtopic.
-
-Daemon:
- %(script)s script needs to connect to an external daemon for send
- notifications, which can be used in localhost or remotely.
- Download the daemon from:
- %(daemon_url)s
- and check its help with ./%(daemon)s --help.
- See also help in script file.
-""" %dict(script=SCRIPT_NAME, daemon_url=DAEMON_URL, daemon=DAEMON)
- ,'test|notify|restart|quit', 'cmd_notify', '')
-
- weechat.hook_config('plugins.var.python.%s.ignore_*' %SCRIPT_NAME, 'ignore_update', '')
- weechat.hook_config('plugins.var.python.%s.server_*' %SCRIPT_NAME, 'server_update', '')
-
- weechat.hook_print('', 'notify_message', '', 1, 'notify_msg', workaround)
- weechat.hook_print('', 'notify_private', '', 1, 'notify_msg', workaround)
-
-
-# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: