# -*- coding: utf-8 -*- ### # Copyright (c) 2009-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 . ### ### # 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 # version 0.1.4: change hook_print callback argument type of # displayed/highlight (WeeChat >= 1.0) # 2011-11-02, Sebastien Helleu : # 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 : # 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 " 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 '<%s>' %(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', '
') if nick: if get_config_boolean('color_nick'): nick = color_tag(nick) else: nick = '<%s>' %nick s = '%s %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: