Compare commits
5 Commits
43cfc666e4
...
a5dc3ccb0a
Author | SHA1 | Date |
---|---|---|
Ekdohibs | a5dc3ccb0a | |
Théophile Bastian | 6c874ecd2b | |
Ekdohibs | ccd3cfa906 | |
Théophile Bastian | b7dbfba8f8 | |
Théophile Bastian | c17575ee8f |
35
README.md
35
README.md
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
225
weenotify.py
225
weenotify.py
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue