weenotify/weenotify.py
2016-04-01 23:59:13 +02:00

254 lines
8.7 KiB
Python

#!/usr/bin/python3
"""
WeeNotify
A minimalist Weechat client using the Weechat relay protocol to
retrieve notifications from a bouncer and display them locally.
---
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/>.
"""
import argparse
import logging
import os
import shlex
import signal
import socket
import subprocess
import sys
import time
import packetRead
##################### CONFIGURATION ##########################
DEFAULT_CONF=(os.path.expanduser("~"))+'/.weenotifyrc'
##################### END CONFIGURATION ######################
def expandPaths(path):
return os.path.expanduser(path)
def safeCall(callArray):
if(len(callArray) == 0):
logging.error("Trying to call an unspecified external program.")
return
try:
subprocess.call(shlex.split(callArray[0])+callArray[1:])
except:
logging.error("Could not execute "+callArray[0])
def gotHighlight(message, nick, conf):
if not 'highlight-action' in conf or not conf['highlight-action']:
return # No action defined: do nothing.
highlightProcessCmd = expandPaths(conf['highlight-action'])
safeCall([highlightProcessCmd, message, nick])
def gotPrivMsg(message, nick, conf):
if not 'privmsg-action' in conf or not conf['privmsg-action']:
return # No action defined: do nothing.
privmsgProcessCmd = expandPaths(conf['privmsg-action'])
safeCall([privmsgProcessCmd, message, nick])
def getResponse(sock, conf):
READ_AT_ONCE=4096
sockBytes = sock.recv(READ_AT_ONCE)
if not sockBytes:
return False # Connection closed
if(len(sockBytes) < 5):
logging.warning("Packet shorter than 5 bytes received. Ignoring.")
return True
if sockBytes[4] != 0:
logging.warning("Received compressed message. Ignoring.")
return True
mLen,_ = packetRead.read_int(sockBytes)
lastPacket = sockBytes
while(len(sockBytes) < mLen):
if(len(lastPacket) < READ_AT_ONCE):
logging.warning("Incomplete packet received. Ignoring.")
return True
lastPacket = sock.recv(READ_AT_ONCE)
sockBytes += lastPacket
body = sockBytes[5:]
ident,body = packetRead.read_str(body)
if ident != "_buffer_line_added":
return True
logging.debug("Received buffer line.")
dataTyp,body = packetRead.read_typ(body)
if(dataTyp != "hda"):
logging.warning("Unknown buffer_line_added format. Ignoring.")
return True
hdaData,body = packetRead.read_hda(body)
for hda in hdaData:
msg=hda['message']
nick=""
for tag in hda['tags_array']:
if tag.startswith('nick_'):
nick = tag[5:]
if hda['highlight'] > 0:
gotHighlight(msg, nick, conf)
continue
for tag in hda['tags_array']:
if tag.startswith('notify_'):
notifLevel = tag[7:]
if notifLevel == 'private':
gotPrivMsg(msg, nick, conf)
break
return True
CONFIG_ITEMS = [
('-c','config', 'Use the given configuration file.', DEFAULT_CONF),
('-s','server', 'Address of the Weechat relay.'),
('-p','port', 'Port of the Weechat relay.'),
('','ensure-background', 'Runs the following command in the background.'+\
' Periodically checks whether it is still open, reruns it if '+\
'necessary, and resets the connection to the server if it was lost '+\
'in the process. Mostly useful to establish a SSH tunnel.'),
('','reconnect-delay','Delay between two attempts to reconnect after '+\
'being disconnected from the server.', '10'),
('-a','highlight-action', 'Program to invoke when highlighted.'),
('','privmsg-action', 'Program to invoke when receiving a private message.'),
('','log-file', 'Log file. If omitted, the logs will be directly printed.')
]
def readConfig(path, createIfAbsent=False):
outDict = dict()
try:
with open(path,'r') as handle:
confOpts = [ x[1] for x in CONFIG_ITEMS ]
for line in handle:
if '#' in line:
line = line[:line.index('#')].strip()
if(line == ''):
continue
if '=' in line:
eqPos = line.index('=')
attr = line[:eqPos].strip()
arg = line[eqPos+1:].strip()
if(attr in confOpts): # Valid option
outDict[attr] = arg
else:
logging.warning('Unknown option: '+attr+'.')
handle.close()
except FileNotFoundError:
if(createIfAbsent):
with open(path, 'x') as touchHandle:
pass
else:
logging.error("The configuration file '"+path+"' does not exists.")
except IOError:
logging.error("Could not read the configuration file at '"+path+"'.")
return outDict
def readCommandLine():
parser = argparse.ArgumentParser(description="WeeChat client to get "+\
"highlight notifications from a distant bouncer.")
parser.add_argument('-v', action='store_true')
for cfgItem in CONFIG_ITEMS:
shortOpt,longOpt,helpMsg,dft = cfgItem[0],cfgItem[1],cfgItem[2],None
if len(cfgItem) >= 4:
dft = cfgItem[3]
if shortOpt == '':
parser.add_argument('--'+longOpt, dest=longOpt, help=helpMsg,\
default=dft)
else:
parser.add_argument(shortOpt, '--'+longOpt, dest=longOpt,\
help=helpMsg, default=dft)
parsed = parser.parse_args()
parsedTable = vars(parsed)
if(parsed.config != None):
parsedTable.update(readConfig(parsed.config))
return parsedTable
def sigint(sig, frame):
logging.info("Stopped.")
exit(0)
def ensureBackgroundCheckRun(proc,conf):
""" Runs (or re-runs if it has terminated) the 'ensure-background'
option command-line if it was specified. """
if not 'ensure-background' in conf or not conf['ensure-background']:
return
if proc == None or proc.poll() != None: # Not started or terminated
if proc != None: # Proc has died.
logging.warning("Background process has died.")
logging.info("Starting background process...")
proc = subprocess.Popen(shlex.split(conf['ensure-background']))
time.sleep(0.5) # Wait a little to let it settle.
return proc
def main():
conf = readCommandLine()
conf.update(readConfig(conf['config'],True))
#TODO command line prevails
if 'log-file' not in conf:
conf['log-file']=None
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s',\
datefmt='%H:%M:%S', filename=conf['log-file'])
logging.getLogger().setLevel(logging.INFO)
if('log-file' in conf):
logging.basicConfig(filename=conf['log-file'])
if('v' in conf and conf['v']): # Verbose
logging.getLogger().setLevel(logging.DEBUG)
logging.info("Verbose mode.")
if not 'server' in conf or not conf['server'] or\
not 'port' in conf or not conf['port']:
print("Missing argument(s): server address and/or port.")
exit(1)
signal.signal(signal.SIGINT, sigint)
signal.signal(signal.SIGTERM, sigint)
bgProcess = None
logging.info("Entering main loop.")
while True:
try:
bgProcess = ensureBackgroundCheckRun(bgProcess, conf)
sock = socket.socket()
logging.info("Connecting to "+conf['server']+":"+conf['port']+"...")
sock.connect((conf['server'], int(conf['port'])))
logging.info("Connected")
sock.sendall(b'init compression=off\n')
sock.sendall(b'sync *\n')
while getResponse(sock,conf):
bgProcess = ensureBackgroundCheckRun(bgProcess, conf)
logging.warning("Connection lost. Retrying...")
except ConnectionRefusedError:
logging.error("Connection refused. Retrying...")
except socket.error as exn:
logging.error("Connection error: %s. Retrying..." % exn)
time.sleep(conf['reconnect-delay'])
if __name__=='__main__':
main()