Compare commits

...

5 Commits

Author SHA1 Message Date
Ekdohibs a5dc3ccb0a Put the relay client in a thread, and get buffer information 2016-04-04 18:18:45 +02:00
Théophile Bastian 6c874ecd2b Merge pull request #1 from Ekdohibs/password
Handle relay password
2016-04-04 12:32:43 +02:00
Ekdohibs ccd3cfa906 Handle relay password 2016-04-04 11:09:31 +02:00
Théophile Bastian b7dbfba8f8 FIX: cast to float for sleep time 2016-04-02 01:48:25 +02:00
Théophile Bastian c17575ee8f Update README.md
Documented the program.
2016-04-02 01:44:32 +02:00
3 changed files with 182 additions and 86 deletions

View File

@ -1,2 +1,37 @@
# weenotify
A minimalist Weechat client using the Weechat relay protocol to retrieve notifications from a bouncer and display them locally.
## Disclaimer
This program does not intend to be robust. That is, it will most certainly crash if you do not configure it properly, or feed it weird data. It does only intend to be a simply-written, working notification gatherer for Weechat.
This program does only support *unencrypted* Weechat Relay protocol. That is, your password and IRC data *will be transmitted without encryption*. Thus, it is *most advised* to connect it to your Weechat Relay *through a SSH/SSL/anything-that-encrypts tunnel*. The `ensure-background` option (see below) makes it really easy, use it!
## Running
### By hand
You can run this client simply by running ``./weenotify.py`` with the right options (see below).
### As a systemd user daemon
You can also use a systemd user daemon to automatically run weenotify in the background: see for instance https://wiki.archlinux.org/index.php/Systemd/User.
A basic systemd service file can be found in `systemd/`: you have to edit it to choose your install path in it. Then, place the weenotify.service file in ~/.local/share/systemd/user/. You can control weenotify with `systemctl --user X weenotify`, where `X` is either `start`, `stop`, `restart`, `enable`, `status`, ...
## Configuration
Each of these options can be passed, prefixed with `--`, directly through the command line, or be saved in a configuration file. The default configuration file (loaded if no configuration file is specified) is `~/.weenotifyrc`.
* `server`: address of the Weechat relay.
* `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: eg., to ensure that a SSH tunnel will be opened and closed with the application, set `ensure-background` to `ssh irc@example.com -L [LOCALPORT]:localhost:[RELAYPORT] -N`.
* `reconnect-delay`: delay between two attempts to reconnect after being disconnected from the server.
* `highlight-action`: program to invoke when highlighted. It will be called with the IRC line that triggered the highlight as its first argument, the message sender as its second argument, and the buffer name as its third.
* `privmsg-action`: program to invoke when receiving a private message. Has the same behavior as `highlight-action`.
* `log-file`: log file path. If omitted, the logs will be directly printed.
The configuration file itself has a very simple syntax: to set the property [property] to the value [value], add the line `[property]=[value]`. A comment starts with a `#` and spans to the end of the line.
You can also pass to the program a few parameters that have no equivalent config file property:
* `-h`: display a short help message and exits,
* `-v`: verbose mode, turns on debug log messages,
* `-c` or `--config`: specifies a configuration file that will be read instead of the default configuration file.
Note that a command line option will always prevail on a configuration file option, shall there be a conflict.

View File

@ -34,7 +34,8 @@ def read_str(data):
def read_ptr(data):
ptrLen = data[0]
return 0,data[ptrLen+1:] # FIXME not implemented. Do we need it?
ptrData = data[1:ptrLen+1]
return int(ptrData.decode('utf-8'), 16), data[ptrLen+1:]
def read_tim(data):
timLen = data[0]
@ -73,8 +74,11 @@ def read_hda(data):
out = []
for dataSet in range(count):
curSet = dict()
path = []
for k in range(len(hpathSplit)):
_,data = read_ptr(data)
ptr, data = read_ptr(data)
path.append(ptr)
curSet['__path'] = path
for pair in keysArray:
curSet[pair[0]],data = pair[1](data)
out.append(curSet)

View File

@ -29,6 +29,7 @@ import socket
import subprocess
import sys
import time
import threading
import packetRead
@ -48,75 +49,143 @@ def safeCall(callArray):
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.
class RelayClient(threading.Thread):
def __init__(self, conf):
threading.Thread.__init__(self)
self.daemon = True # Stop when the program terminates
self.conf = conf
self.sock = None
self.packet_actions = {
'ask_buffers' : self.asked_buffers,
'_buffer_line_added' : self.buffer_line_added
}
self.buffers = {}
logging.debug("Notifying highlight message.")
highlightProcessCmd = expandPaths(conf['highlight-action'])
safeCall([highlightProcessCmd, message, nick])
def run(self):
self.connect()
while True:
READ_AT_ONCE = 4096
data = self.recv(READ_AT_ONCE)
if len(data) < 5:
logging.warning("Packet shorter than 5 bytes received. Ignoring.")
continue
def gotPrivMsg(message, nick, conf):
if not 'privmsg-action' in conf or not conf['privmsg-action']:
return # No action defined: do nothing.
logging.debug("Notifying private message.")
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)
dataLen, _ = packetRead.read_int(data)
lastPacket = data
while len(data) < dataLen:
if len(lastPacket) < READ_AT_ONCE:
logging.warning("Incomplete packet received. Ignoring.")
break
lastPacket = self.recv(READ_AT_ONCE)
data += lastPacket
if len(data) < dataLen:
continue
self.process_packet(data)
def process_packet(self, packet):
if packet[4] != 0:
logging.warning("Received compressed message. Ignoring.")
return
body = packet[5:]
ident, body = packetRead.read_str(body)
if ident in self.packet_actions:
self.packet_actions[ident](body)
def connect(self):
while True:
try:
self.sock = socket.socket()
logging.info("Connecting to " + self.conf['server'] + ":" + self.conf['port'] + "...")
self.sock.connect((self.conf['server'], int(self.conf['port'])))
logging.info("Connected")
self.init_connection()
return
except ConnectionRefusedError:
self.sock = None
logging.error("Connection refused. Retrying...")
except socket.error as exn:
self.sock = None
logging.error("Connection error: %s. Retrying..." % exn)
time.sleep(float(self.conf['reconnect-delay']))
def init_connection(self):
password = self.conf.get('password', None)
if password != None:
self.sock.sendall(b'init compression=off,password='+password.encode("utf-8")+b'\n')
else:
self.sock.sendall(b'init compression=off\n')
self.sock.sendall(b'sync *\n')
# Ask for name of buffers
self.sock.sendall(b'(ask_buffers) hdata buffer:gui_buffers(*) name\n')
def recv(self, n):
while True:
try:
data = self.sock.recv(n)
if data:
return data
logging.warning("Connection lost. Retrying...")
except socked.error as exn:
logging.error("Connection error: %s. Retrying..." % exn)
self.connect()
def asked_buffers(self, body):
data_type, body = packetRead.read_typ(body)
if(data_type != "hda"):
logging.warning("Unknown asked_buffers format. Ignoring.")
return
hdaData, _ = packetRead.read_hda(body)
for hda in hdaData:
self.buffers[hda['__path'][-1]] = hda['name']
def buffer_line_added(self, body):
data_type, body = packetRead.read_typ(body)
if(data_type != "hda"):
logging.warning("Unknown buffer_line_added format. Ignoring.")
return
hdaData, _ = packetRead.read_hda(body)
for hda in hdaData:
msg = hda['message']
buffer = hda.get('buffer', 0)
if buffer not in self.buffers:
self.sock.sendall(b'(ask_buffers) hdata buffer:gui_buffers(*) name\n')
buffer_name = '<unknown>'
else:
buffer_name = self.buffers[buffer]
nick = ""
for tag in hda['tags_array']:
if tag.startswith('nick_'):
nick = tag[5:]
if hda['highlight'] > 0:
self.gotHighlight(msg, nick, buffer_name)
continue
for tag in hda['tags_array']:
if tag.startswith('notify_'):
notifLevel = tag[7:]
if notifLevel == 'private':
self.gotPrivMsg(msg, nick, buffer_name)
break
def gotHighlight(self, message, nick, buffer_name):
if not selt.conf.get('highlight-action', None):
return # No action defined: do nothing.
logging.debug("Notifying highlight message.")
highlightProcessCmd = expandPaths(self.conf['highlight-action'])
safeCall([highlightProcessCmd, message, nick, buffer_name])
def gotPrivMsg(self, message, nick, buffer_name):
if not self.conf.get('privmsg-action', None):
return # No action defined: do nothing.
logging.debug("Notifying private message.")
privmsgProcessCmd = expandPaths(self.conf['privmsg-action'])
safeCall([privmsgProcessCmd, message, nick, buffer_name])
return True
CONFIG_ITEMS = [
('-c','config', 'Use the given configuration file.', DEFAULT_CONF),
@ -130,7 +199,8 @@ CONFIG_ITEMS = [
'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.')
('','log-file', 'Log file. If omitted, the logs will be directly printed.'),
('','password', 'Relay password')
]
def readConfig(path, createIfAbsent=False):
@ -244,27 +314,14 @@ def main():
signal.signal(signal.SIGINT, sigint)
signal.signal(signal.SIGTERM, sigint)
bgProcess = None
client = RelayClient(conf)
bgProcess = ensureBackgroundCheckRun(None, conf)
logging.info("Entering main loop.")
client.start()
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'])
bgProcess = ensureBackgroundCheckRun(bgProcess, conf)
time.sleep(0.5)
if __name__=='__main__':
main()