summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL.md34
-rw-r--r--README.md42
-rwxr-xr-xbeinc_generic_client.py269
-rwxr-xr-xbeinc_pull.py (renamed from beinc_poller.py)144
-rwxr-xr-xbeinc_server.py255
-rw-r--r--beinc_weechat.py416
6 files changed, 551 insertions, 609 deletions
diff --git a/INSTALL.md b/INSTALL.md
index b0e8a0a..15bcb35 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,38 +1,38 @@
1## Installation Instructions 1# Installation Instructions
2 2
3Refer to README for a basic information about the different BEINC components 3Refer to README for a basic information about the different BEINC components
4and their software requirements! 4and their software requirements!
5 5
6 6
7### Installing the dependencies 7## Installing the dependencies
8 8
9#### Gentoo GNU/Linux 2014, 2015 9### Gentoo GNU/Linux
10 10
11# emerge -va dev-python/pyopenssl dev-python/notify-python 11 ```bash
12 emerge -va dev-python/notify2
13 ```
12 14
13 15
14#### Ubuntu GNU/Linux 16### Ubuntu GNU/Linux
15 17
16# apt-get install python-openssl python-notify2 18 ```bash
19 apt-get install python-notify2
20 ```
17 21
18 22
19#### FreeBSD 10, 11 23### FreeBSD 12
20
21# cd /usr/ports/security/py-openssl
22# make install clean
23 24
24# cd /usr/ports/devel/py-notify 25# cd /usr/ports/devel/py-notify
25# make install clean 26# make install clean
26 27
27 28
28#### Intallation using virtualenv and pip 29### Intallation using virtualenv and pip
29 30
30Very often you are not the administrator of the server hosting your BEINC server 31Very often you are not the administrator of the server hosting your BEINC server
31This is where virtualenv may become handy (provided that it has been installed): 32This is where virtualenv may become handy (provided that it has been installed):
32$ virtualenv beinc_installation 33$ virtualenv beinc_installation
33$ cd beinc_installation 34$ cd beinc_installation
34$ source bin/activate 35$ source bin/activate
35$ pip install pyopenssl
36$ cd beinc-<version> 36$ cd beinc-<version>
37$ ./beinc_server.py -h 37$ ./beinc_server.py -h
38 38
@@ -59,13 +59,13 @@ The example loads the server listening on 10.0.0.44, port 9678 using the default
59~/.beinc_server.json as a configuration file and usually gives you the Twisted 59~/.beinc_server.json as a configuration file and usually gives you the Twisted
60log console 60log console
61 61
62beinc_poller.py: 62beinc_pull.py:
63$ ./beinc_poller.py -h 63$ ./beinc_pull.py -h
64The example lists all available command options 64The example lists all available command options
65$ ./beinc_poller.py -n weechat -t 10 -f 7 --cert-file /home/sgs/weechat_cert.crt \ 65$ ./beinc_pull.py -n weechat -t 10 -f 7 --cert-file /home/sgs/weechat_cert.crt \
66 -p /home/sgs/.weechat_beinc_passwd https://10.0.0.44:9678 66 -p /home/sgs/.weechat_beinc_passwd https://10.0.0.44:9678
67Starts polling from the instance (target) defined above, 67Starts pulling from the instance (target) defined above,
68defining an OSD timeout of 10 seconds, polling frequency of 7 seconds, 68defining an OSD timeout of 10 seconds, pulling frequency of 7 seconds,
69checking whether the server's SSL certificate was signed (issued) by /home/sgs/weechat_cert.crt, 69checking whether the server's SSL certificate was signed (issued) by /home/sgs/weechat_cert.crt,
70extracting the target's password from /home/sgs/.weechat_beinc_passwd 70extracting the target's password from /home/sgs/.weechat_beinc_passwd
71 71
diff --git a/README.md b/README.md
index 388562b..200e29d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
1Copyright (C) 2014-2018 - Simeon Simeonov 1Copyright (C) 2014-2020 - Simeon Simeonov
2See the end of the file for license conditions. 2See the end of the file for license conditions.
3 3
4 4
@@ -10,14 +10,14 @@ on-screen-display notification scenarios.
10 10
11The current version of BEINC contains the following components: 11The current version of BEINC contains the following components:
12beinc_server.py - server used for queueing or providing on-screen-display (OSD) 12beinc_server.py - server used for queueing or providing on-screen-display (OSD)
13beinc_poller.py - client used to fetch enqueued messages from beinc_server.py 13beinc_pull.py - client used to fetch enqueued messages from beinc_server.py
14 and provide OSD 14 and provide OSD
15beinc_weechat.py - a complete script / client for the Weechat IRC client >=0.4.0 15beinc_weechat.py - a complete script / client for the Weechat IRC client >=0.4.0
16 used to push notification messages to beinc_server.py 16 used to push notification messages to beinc_server.py
17beinc_generic_client.py - a simple client used to push notification messages 17beinc_generic_client.py - a simple client used to push notification messages
18 to beinc_server.py 18 to beinc_server.py
19 Its main purpose is to provide a convenient way to 19 Its main purpose is to provide a convenient way to
20 test beinc_server.py (and beinc_poller.py) as well as 20 test beinc_server.py (and beinc_pull.py) as well as
21 to serve as an example for how to develop BEINC-clients 21 to serve as an example for how to develop BEINC-clients
22+ documentation and a sample configuration file (beinc_config_sample.json) 22+ documentation and a sample configuration file (beinc_config_sample.json)
23 23
@@ -28,7 +28,7 @@ BEINC is a free-software, licensed under the GPL3. It gives you the freedoms
28of using it, studying it and modifying it. 28of using it, studying it and modifying it.
29 29
30BEINC is designed to assist you two common scenarios. 30BEINC is designed to assist you two common scenarios.
31As an example we will consider the case of an IRC cliet, although BEINC can be 31As an example we will consider the case of an IRC client, although BEINC can be
32used by any client that conforms with BEINC's "messaging protocol". 32used by any client that conforms with BEINC's "messaging protocol".
33 33
34We have the common situation where a user is accessing an IRC client located on 34We have the common situation where a user is accessing an IRC client located on
@@ -66,20 +66,20 @@ Solution:
66 - set up the beinc_server.py on it. No OSD capabilities are needed (no X11). 66 - set up the beinc_server.py on it. No OSD capabilities are needed (no X11).
67 Define instance(s) for queueing. Define a queue size 67 Define instance(s) for queueing. Define a queue size
68 (how many notifications to store) for each of them. 68 (how many notifications to store) for each of them.
69 - set up beinc_poller.py on the desktop workstation (the computer where the 69 - set up beinc_pull.py on the desktop workstation (the computer where the
70 notification-message should be displayed). 70 notification-message should be displayed).
71 beinc_poller.py should be able to access the server running beinc_server.py 71 beinc_pull.py should be able to access the server running beinc_server.py
72 Polling interval of 5 seconds should be enough for most users. 72 Pulling interval of 5 seconds should be enough for most users.
73 73
74Read beinc_config_sample.json.readme for details on how to setup 74Read beinc_config_sample.json.readme for details on how to setup
75beinc_server.py and beinc_weechat.py! 75beinc_server.py and beinc_weechat.py!
76Use "-h" command line parameter to display the available options for 76Use "-h" command line parameter to display the available options for
77beinc_poller.py and beinc_generic_client.py 77beinc_pull.py and beinc_generic_client.py
78Each configured beinc_server.py instance has an unique name. 78Each configured beinc_server.py instance has an unique name.
79There are 2 operations that can be done on an instance: 79There are 2 operations that can be done on an instance:
80 - push - a client is sending a notification to the BEINC server. 80 - push - a client is sending a notification to the BEINC server.
81 A push is available for all instances. 81 A push is available for all instances.
82 - pull - a poller (f.i. beinc_poller.py) is fetching data from the instance-queue. 82 - pull - a pull (f.i. beinc_pull.py) is fetching data from the instance-queue.
83 A pull is available only for instances defined for queueing. 83 A pull is available only for instances defined for queueing.
84 (read beinc_config_sample.json.readme for details!) 84 (read beinc_config_sample.json.readme for details!)
85Example: 85Example:
@@ -91,28 +91,38 @@ your URL will be: https://hostname:port
91 91
92Any system running the software required for the selected components. 92Any system running the software required for the selected components.
93All components tested on: Gentoo GNU/Linux, 93All components tested on: Gentoo GNU/Linux,
94 Ubuntu GNU/Linux 16.4, 18.4, 94 Ubuntu GNU/Linux 18.4,
95 FreeBSD 10.x, 11.x 95 FreeBSD 12.x
96 96
97 97
98### Requirements 98### Requirements
99All components: Python >= 2.7.9 or Python >= 3.4.* 99All components: Python >= 3.6.*
100 100
101beinc_server.py: pynotify >= 0.1 (optional) 101beinc_server.py: pynotify >= 0.1 (optional)
102beinc_weechat.py: Weechat >= 0.4.0 102beinc_weechat.py: Weechat >= 0.4.0
103beinc_poller.py: pynotify >= 0.1 103beinc_pull.py: pynotify >= 0.1
104beinc_generic_client.py: No additional software required 104beinc_generic_client.py: No additional software required
105 105
106Read INSTALL in this very same folder for more details about installing the requirements! 106Read INSTALL in this very same folder for more details about installing the requirements!
107 107
108 108
109### License 109## Support and contributing
110
111BEINC is hosted on GitHub: https://github.com/blackm0re/BEINC
112
113
114## Author
115
116Simeon Simeonov - sgs @ Freenode
117
118
119## License
110 120
111This file is part of BEINC. 121This file is part of BEINC.
112 122
113BEINC is free software: you can redistribute it and/or modify 123BEINC is free software: you can redistribute it and/or modify
114it under the terms of the GNU General Public License as published by 124it under the terms of the GNU General Public License as published by
115the Free Software Foundation, either version 3 of the License, or 125the Free Software Foundation, either version 3 of the License, or
116(at your option) any later version. 126(at your option) any later version.
117 127
118This program is distributed in the hope that it will be useful, 128This program is distributed in the hope that it will be useful,
diff --git a/beinc_generic_client.py b/beinc_generic_client.py
index 4d8fc35..5514642 100755
--- a/beinc_generic_client.py
+++ b/beinc_generic_client.py
@@ -1,8 +1,8 @@
1#!/usr/bin/env python 1#!/usr/bin/env python
2# -*- coding: utf-8 -*- 2# -*- coding: utf-8 -*-
3 3
4# Blackmore's Enhanced IRC-Notification Collection (BEINC) v2.0 4# Blackmore's Enhanced IRC-Notification Collection (BEINC) v4.0
5# Copyright (C) 2013-2018 Simeon Simeonov 5# Copyright (C) 2013-2020 Simeon Simeonov
6 6
7# This program is free software: you can redistribute it and/or modify 7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by 8# it under the terms of the GNU General Public License as published by
@@ -16,146 +16,114 @@
16 16
17# You should have received a copy of the GNU General Public License 17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>. 18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19"""A generic BEINC client that can be used for testing or as a template"""
20
21import argparse 20import argparse
22import errno 21import errno
23import getpass 22import getpass
24import httplib # for Python < 2.7.9 23import io
24import json
25import os 25import os
26import socket 26import socket
27import ssl 27import ssl
28import sys 28import sys
29import xmlrpclib 29import urllib.parse
30import urllib.request
30 31
31 32
32__author__ = 'Simeon Simeonov' 33__author__ = 'Simeon Simeonov'
33__version__ = '3.0' 34__version__ = '4.0'
34__license__ = 'GPL3' 35__license__ = 'GPL3'
35 36
36 37
37BEINC_SSL_METHODS = {'TLSv1': ssl.PROTOCOL_TLSv1} 38def eprint(*arg, **kwargs):
38try: 39 """stdderr print wrapper"""
39 BEINC_SSL_METHODS.update({'TLSv1_1': ssl.PROTOCOL_TLSv1_1}) 40 print(*arg, file=sys.stderr, flush=True, **kwargs)
40 BEINC_SSL_METHODS.update({'TLSv1_2': ssl.PROTOCOL_TLSv1_2})
41except:
42 pass
43 41
44 42
45class BEINCCustomHTTPSConnection(httplib.HTTPConnection): 43def fetch_password(args_password):
46 """ 44 """
47 This class allows communication via SSL. 45 Fetches the password from the provided `args_password`
46
47 :param args_password: The password coming from argparse
48 :type args_password: str
48 49
49 It is a reimplementation of httplib.HTTPSConnection and 50 :return: The password string
50 allows the server certificate to be validated against CA 51 :rtype: str
51 This functionality lacks in Python < 2.7.9
52 """ 52 """
53 default_port = httplib.HTTPS_PORT 53 if not args_password:
54 try:
55 return getpass.getpass()
56 except KeyboardInterrupt:
57 eprint(os.linesep + 'Prompt terminated')
58 sys.exit(errno.EACCES)
59 elif os.path.isfile(args_password):
60 try:
61 with io.open(args_password, 'r') as fp:
62 passwd = fp.readline()
63 if passwd.strip():
64 return passwd.strip()
65 except Exception as e:
66 eprint(f'Unable to open password file: {e}')
67 sys.exit(1)
68 return args_password
54 69
55 def __init__(self, host, port=None, key_file=None, cert_file=None,
56 strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
57 source_address=None, custom_ssl_options={}):
58 httplib.HTTPConnection.__init__(self, host, port, strict, timeout,
59 source_address)
60 self.key_file = key_file
61 self.cert_file = cert_file
62 self.custom_ssl_options = custom_ssl_options
63 70
64 def connect(self): 71def pull_notifications(ssl_context, args):
65 "Connect to a host on a given (SSL) port." 72 """
66 sock = socket.create_connection((self.host, self.port), 73 Pulls the notifications from the BEINC server.
67 self.timeout, self.source_address)
68 if self._tunnel_host:
69 self.sock = sock
70 self._tunnel()
71 self.sock = ssl.wrap_socket(sock,
72 self.key_file,
73 self.cert_file,
74 **self.custom_ssl_options)
75 74
75 :param ssl_context: The SSL context object
76 :type ssl_context: ssl.SSLContext
76 77
77class BEINCCustomSafeTransport(xmlrpclib.Transport): 78 :param args: The arguments assigned from argparse
78 """ 79 :type args: argparse.Namespace
79 Provides an alternative HTTPS transport for clients running on Python<2.7.9 80
81 :return: The notifications (if any)
82 :rtype: list
80 """ 83 """
84 response = urllib.request.urlopen(
85 args.url,
86 data=urllib.parse.urlencode(
87 (
88 ('resource_name', args.rname),
89 ('password', args.password)
90 )).encode('utf-8'),
91 timeout=args.socket_timeout,
92 context=ssl_context)
93 response_dict = json.loads(response.read().decode('utf-8'))
94 if response.code != 200:
95 raise socket.error(response_dict.get('message', ''))
96 return response_dict['data']['messages']
81 97
82 def __init__(self, use_datetime=0, custom_ssl_options={}):
83 xmlrpclib.Transport.__init__(self, use_datetime=use_datetime)
84 self.custom_ssl_options = custom_ssl_options
85 98
86 def make_connection(self, host): 99def push_notification(ssl_context, args):
87 if self._connection and host == self._connection[0]: 100 """
88 return self._connection[1] 101 Pushes a single notification to the BEINC server.
89 try:
90 HTTPS = BEINCCustomHTTPSConnection
91 except AttributeError:
92 raise NotImplementedError(
93 "your version of httplib doesn't support HTTPS"
94 )
95 else:
96 chost, self._extra_headers, x509 = self.get_host_info(host)
97 self._connection = host, HTTPS(
98 chost,
99 None,
100 custom_ssl_options=self.custom_ssl_options,
101 **(x509 or {}))
102 return self._connection[1]
103 102
103 :param ssl_context: The SSL context object
104 :type ssl_context: ssl.SSLContext
104 105
105def action_execute(args): 106 :param args: The arguments assigned from argparse
106 """ 107 :type args: argparse.Namespace
107 """ 108 """
108 try: 109 response = urllib.request.urlopen(
109 ssl_version = BEINC_SSL_METHODS.get(args.ssl_version, 110 args.url,
110 ssl.PROTOCOL_SSLv23) 111 data=urllib.parse.urlencode(
111 if sys.hexversion >= 0x20709f0: 112 (
112 # Python >= 2.7.9 113 ('resource_name', args.rname),
113 context = ssl.SSLContext(ssl_version) 114 ('password', args.password),
114 context.verify_mode = ssl.CERT_NONE 115 ('title', args.title),
115 if args.cert: 116 ('message', args.message)
116 context.verify_mode = ssl.CERT_REQUIRED 117 )).encode('utf-8'),
117 context.load_verify_locations(os.path.expanduser(args.cert)) 118 timeout=args.socket_timeout,
118 context.check_hostname = bool(not args.disable_hostname_check) 119 context=ssl_context)
119 if args.ciphers: 120 response_dict = json.loads(response.read().decode('utf-8'))
120 context.set_ciphers(args.ciphers) 121 if response.code != 200:
121 transport = xmlrpclib.SafeTransport(context=context) 122 raise socket.error(response_dict.get('message', ''))
122 else:
123 # Python < 2.7.9
124 ssl_options = {}
125 ssl_options['ssl_version'] = ssl_version
126 if args.cert:
127 ssl_options['ca_certs'] = os.path.expanduser(args.cert)
128 ssl_options['cert_reqs'] = ssl.CERT_REQUIRED
129 if args.ciphers:
130 ssl_options['ciphers'] = args.ciphers
131 transport = BEINCCustomSafeTransport(
132 custom_ssl_options=ssl_options)
133 server = xmlrpclib.ServerProxy(args.url,
134 transport=transport)
135 if args.pull:
136 print(server.pull(args.rname, args.password))
137 else:
138 print(server.push(args.rname,
139 args.password,
140 args.title,
141 args.message))
142 except xmlrpclib.Fault as fault:
143 sys.stderr.write(
144 'BEINC server answered with errorCode={0}: {1}\n'.format(
145 fault.faultCode,
146 fault.faultString))
147 except ssl.SSLError as e:
148 sys.stderr.write('BEINC SSL/TLS error: {0}\n'.format(e))
149 sys.exit(errno.EPERM)
150 except socket.error as e:
151 sys.stderr.write('BEINC connection error: {0}\n'.format(e))
152 sys.exit(errno.EPERM)
153 except Exception as e:
154 sys.stderr.write('BEINC generic client error: {0}\n'.format(e))
155 sys.exit(errno.EPERM)
156 123
157 124
158def main(): 125def main(inargs=None):
126 """main entry"""
159 parser = argparse.ArgumentParser( 127 parser = argparse.ArgumentParser(
160 description='The following options are available') 128 description='The following options are available')
161 parser.add_argument( 129 parser.add_argument(
@@ -178,13 +146,12 @@ def main():
178 dest='ciphers', 146 dest='ciphers',
179 default='', 147 default='',
180 help='Preferred ciphers list (default: auto)') 148 help='Preferred ciphers list (default: auto)')
181 if sys.hexversion >= 0x20709f0: 149 parser.add_argument(
182 parser.add_argument( 150 '--disable-hostname-check',
183 '--disable-hostname-check', 151 action='store_true',
184 action='store_true', 152 dest='disable_hostname_check',
185 dest='disable_hostname_check', 153 default=False,
186 default=False, 154 help='Do not check whether server cert matches server hostname')
187 help='Do not check whether server cert matches server hostname')
188 parser.add_argument( 155 parser.add_argument(
189 '-m', '--message', 156 '-m', '--message',
190 metavar='MESSAGE', 157 metavar='MESSAGE',
@@ -213,23 +180,6 @@ def main():
213 dest='pull', 180 dest='pull',
214 default=False, 181 default=False,
215 help='Perform a pull operation (default: push)') 182 help='Perform a pull operation (default: push)')
216 if sys.hexversion >= 0x20709f0:
217 parser.add_argument(
218 '-s', '--ssl-version',
219 metavar='VERSION',
220 type=str,
221 dest='ssl_version',
222 default='auto',
223 help='Use SSL version: "auto" (default), '
224 '"SSLv3", "TLSv1", "TLSv1_1", "TLSv1_2"')
225 else:
226 parser.add_argument(
227 '-s', '--ssl-version',
228 metavar='VERSION',
229 type=str,
230 dest='ssl_version',
231 default='auto',
232 help='Use SSL version: "auto" (default), "SSLv3", "TLSv1"')
233 parser.add_argument( 183 parser.add_argument(
234 '-T', '--socket-timeout', 184 '-T', '--socket-timeout',
235 metavar='SECONDS', 185 metavar='SECONDS',
@@ -247,29 +197,36 @@ def main():
247 parser.add_argument( 197 parser.add_argument(
248 '-v', '--version', 198 '-v', '--version',
249 action='version', 199 action='version',
250 version='%(prog)s {0}'.format(__version__), 200 version=f'%(prog)s {__version__}',
251 help='display program-version and exit') 201 help='display program-version and exit')
252 args = parser.parse_args() 202 args = parser.parse_args(inargs)
253
254 if not args.password:
255 try:
256 args.password = getpass.getpass()
257 except Exception as e:
258 sys.stderr.write('Prompt terminated\n')
259 sys.exit(errno.EACCES)
260 elif os.path.isfile(args.password):
261 try:
262 with open(args.password, 'r') as fp:
263 passwd = fp.readline()
264 if passwd.strip():
265 args.password = passwd.strip()
266 except Exception as e:
267 sys.stderr.write('Unable to open password file: {0}'.format(e))
268 sys.exit(1)
269 if args.socket_timeout: 203 if args.socket_timeout:
270 socket.setdefaulttimeout(args.socket_timeout) 204 socket.setdefaulttimeout(args.socket_timeout)
271 action_execute(args) 205 args.password = fetch_password(args.password)
272 sys.exit(0) 206 try:
207 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
208 context.verify_mode = ssl.CERT_NONE
209 if args.cert:
210 context.verify_mode = ssl.CERT_REQUIRED
211 context.load_verify_locations(cafile=os.path.expanduser(args.cert))
212 context.check_hostname = bool(not args.disable_hostname_check)
213 if args.ciphers:
214 context.set_ciphers(args.ciphers)
215 if args.pull:
216 print(json.dumps(pull_notifications(context, args), indent=4))
217 else:
218 push_notification(context, args)
219 print('OK')
220 sys.exit(0)
221 except ssl.SSLError as e:
222 eprint(f'BEINC SSL/TLS error: {e}')
223 sys.exit(errno.EPERM)
224 except socket.error as e:
225 eprint(f'BEINC connection error: {e}')
226 sys.exit(errno.EPERM)
227 except Exception as e:
228 eprint(f'BEINC generic client error: {e}')
229 sys.exit(errno.EPERM)
273 230
274 231
275if __name__ == '__main__': 232if __name__ == '__main__':
diff --git a/beinc_poller.py b/beinc_pull.py
index 624cd24..9c44c00 100755
--- a/beinc_poller.py
+++ b/beinc_pull.py
@@ -1,8 +1,8 @@
1#!/usr/bin/env python 1#!/usr/bin/env python
2# -*- coding: utf-8 -*- 2# -*- coding: utf-8 -*-
3 3
4# Blackmore's Enhanced IRC-Notification Collection (BEINC) v3.0 4# Blackmore's Enhanced IRC-Notification Collection (BEINC) v4.0
5# Copyright (C) 2013-2018 Simeon Simeonov 5# Copyright (C) 2013-2020 Simeon Simeonov
6 6
7# This program is free software: you can redistribute it and/or modify 7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by 8# it under the terms of the GNU General Public License as published by
@@ -16,11 +16,11 @@
16 16
17# You should have received a copy of the GNU General Public License 17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>. 18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19"""A simple client that pulls notifications from a BEINC server"""
20
21import argparse 20import argparse
22import errno 21import errno
23import getpass 22import getpass
23import io
24import json 24import json
25import os 25import os
26import sched 26import sched
@@ -28,69 +28,92 @@ import socket
28import ssl 28import ssl
29import sys 29import sys
30import time 30import time
31 31import urllib.parse
32PY2 = sys.version_info[0] == 2 32import urllib.request
33PY3 = sys.version_info[0] == 3
34
35if PY3:
36 from urllib.parse import urlencode
37 from urllib.request import urlopen
38else:
39 from urllib import urlencode
40 from urllib2 import urlopen
41 33
42try: 34try:
43 if PY3: 35 import notify2 as pynotify
44 import notify2 as pynotify 36except ImportError:
45 else:
46 import pynotify
47except ImportError as e:
48 pynotify = None 37 pynotify = None
49 38
50 39
51__author__ = 'Simeon Simeonov' 40__author__ = 'Simeon Simeonov'
52__version__ = '3.0' 41__version__ = '4.0'
53__license__ = 'GPL3' 42__license__ = 'GPL3'
54 43
55 44
45def eprint(*arg, **kwargs):
46 """stdderr print wrapper"""
47 print(*arg, file=sys.stderr, flush=True, **kwargs)
48
49
50def fetch_password(args_password):
51 """
52 Fetches the password from the provided `args_password`
53
54 :param args_password: The password coming from argparse
55 :type args_password: str
56
57 :return: The password string
58 :rtype: str
59 """
60 if not args_password:
61 try:
62 return getpass.getpass()
63 except KeyboardInterrupt:
64 eprint(os.linesep + 'Prompt terminated')
65 sys.exit(errno.EACCES)
66 elif os.path.isfile(args_password):
67 try:
68 with io.open(args_password, 'r') as fp:
69 passwd = fp.readline()
70 if passwd.strip():
71 return passwd.strip()
72 except Exception as e:
73 eprint(f'Unable to open password file: {e}')
74 sys.exit(1)
75 return args_password
76
77
56def display_notification(args, title, message): 78def display_notification(args, title, message):
57 """ 79 """
58 A wrapper function for displaying a single notification 80 A wrapper function for displaying a single notification
59 81
60 args: the argparse processed command-line arguments 82 :param args: The arguments assigned from argparse
61 title: notification title 83 :type args: argparse.Namespace
62 message: notification message 84
85 :param title: The title
86 :type title: str
87
88 :param message: The message
89 :type message: str
63 """ 90 """
64 if args.osd_sys == 'pynotify': 91 if args.osd_sys == 'pynotify':
65 if not pynotify: 92 if not pynotify:
66 raise Exception( 93 raise Exception(
67 'Could not load "pynotify".\n' 94 'Could not load "pynotify".'
68 'Please install "pynotify" or use a different osd-system!\n' 95 'Please install "pynotify" or use a different osd-system!'
69 'Terminating...\n') 96 'Terminating...')
70 if not pynotify.init('BEINC Notify'): 97 if not pynotify.init('BEINC Notify'):
71 raise Exception('There was a problem with libnotify\n') 98 raise Exception('There was a problem with libnotify')
72 notification_obj = pynotify.Notification(summary=title, 99 notification_obj = pynotify.Notification(summary=title,
73 message=message) 100 message=message)
74 if PY3: 101 notification_obj.timeout = 1000 * args.osd_timeout
75 notification_obj.timeout = 1000 * args.osd_timeout 102 notification_obj.set_category('im.received')
76 notification_obj.set_category('im.received')
77 else:
78 notification_obj.set_timeout(1000 * args.osd_timeout)
79 notification_obj.set_property(
80 'app_name',
81 '{0} {1}'.format(sys.argv[0], __version__))
82 notification_obj.show() 103 notification_obj.show()
83 else: 104 else:
84 raise Exception( 105 raise Exception(f'Unsupported osd-system: {args.osd_sys}')
85 'Unsupported osd-system: {0}\n'.format(args.osd_sys))
86 106
87 107
88def poll_notifications(scheduler, args): 108def pull_notifications(scheduler, args):
89 """ 109 """
90 the core function initiated by the scheduler performing a single poll 110 The core function initiated by the scheduler performing a single pull
91 111
92 scheduler: scheduler object 112 :param scheduler: Scheduler object
93 args: the argparse processed command-line arguments 113 :type scheduler: sched.scheduler
114
115 :param args: The arguments assigned from argparse
116 :type args: argparse.Namespace
94 """ 117 """
95 try: 118 try:
96 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 119 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
@@ -101,9 +124,9 @@ def poll_notifications(scheduler, args):
101 context.check_hostname = bool(not args.disable_hostname_check) 124 context.check_hostname = bool(not args.disable_hostname_check)
102 if args.ciphers: 125 if args.ciphers:
103 context.set_ciphers(args.ciphers) 126 context.set_ciphers(args.ciphers)
104 response = urlopen( 127 response = urllib.request.urlopen(
105 args.url, 128 args.url,
106 data=urlencode( 129 data=urllib.parse.urlencode(
107 ( 130 (
108 ('resource_name', args.rname), 131 ('resource_name', args.rname),
109 ('password', args.password) 132 ('password', args.password)
@@ -120,20 +143,21 @@ def poll_notifications(scheduler, args):
120 response.close() 143 response.close()
121 scheduler.enter(args.frequency, 144 scheduler.enter(args.frequency,
122 1, 145 1,
123 poll_notifications, 146 pull_notifications,
124 (scheduler, args)) 147 (scheduler, args))
125 except ssl.SSLError as e: 148 except ssl.SSLError as e:
126 sys.stderr.write('BEINC SSL/TLS error: {0}\n'.format(e)) 149 eprint(f'BEINC SSL/TLS error: {e}')
127 sys.exit(errno.EPERM) 150 sys.exit(errno.EPERM)
128 except socket.error as e: 151 except socket.error as e:
129 sys.stderr.write('BEINC connection error: {0}\n'.format(e)) 152 eprint(f'BEINC connection error: {e}')
130 sys.exit(errno.EPERM) 153 sys.exit(errno.EPERM)
131 except Exception as e: 154 except Exception as e:
132 sys.stderr.write('BEINC generic client error: {0}\n'.format(e)) 155 eprint(f'BEINC generic client error: {e}')
133 sys.exit(errno.EPERM) 156 sys.exit(errno.EPERM)
134 157
135 158
136def main(): 159def main(inargs=None):
160 """main entry"""
137 parser = argparse.ArgumentParser( 161 parser = argparse.ArgumentParser(
138 description='The following options are available') 162 description='The following options are available')
139 parser.add_argument( 163 parser.add_argument(
@@ -168,7 +192,7 @@ def main():
168 type=int, 192 type=int,
169 dest='frequency', 193 dest='frequency',
170 default=10, 194 default=10,
171 help='Polling frequency in seconds (default: 10)') 195 help='Pulling frequency in seconds (default: 10)')
172 parser.add_argument( 196 parser.add_argument(
173 '-n', '--resource-name', 197 '-n', '--resource-name',
174 metavar='NAME', 198 metavar='NAME',
@@ -208,28 +232,14 @@ def main():
208 parser.add_argument( 232 parser.add_argument(
209 '-v', '--version', 233 '-v', '--version',
210 action='version', 234 action='version',
211 version='%(prog)s {0}'.format(__version__), 235 version=f'%(prog)s {__version__}',
212 help='display program-version and exit') 236 help='display program-version and exit')
213 args = parser.parse_args() 237 args = parser.parse_args(inargs)
214 if not args.password: 238 args.password = fetch_password(args.password)
215 try:
216 args.password = getpass.getpass()
217 except Exception as e:
218 sys.stderr.write('Prompt terminated\n')
219 sys.exit(errno.EACCES)
220 elif os.path.isfile(args.password):
221 try:
222 with open(args.password, 'r') as fp:
223 passwd = fp.readline()
224 if passwd.strip():
225 args.password = passwd.strip()
226 except Exception as e:
227 sys.stderr.write('Unable to open password file: {0}'.format(e))
228 sys.exit(1)
229 scheduler = sched.scheduler(time.time, time.sleep) 239 scheduler = sched.scheduler(time.time, time.sleep)
230 scheduler.enter(args.frequency, 240 scheduler.enter(args.frequency,
231 1, 241 1,
232 poll_notifications, 242 pull_notifications,
233 (scheduler, args)) 243 (scheduler, args))
234 scheduler.run() 244 scheduler.run()
235 245
diff --git a/beinc_server.py b/beinc_server.py
index ad81ce9..0f6c248 100755
--- a/beinc_server.py
+++ b/beinc_server.py
@@ -1,8 +1,8 @@
1#!/usr/bin/env python 1#!/usr/bin/env python
2# -*- coding: utf-8 -*- 2# -*- coding: utf-8 -*-
3 3
4# Blackmore's Enhanced IRC-Notification Collection (BEINC) v3.0 4# Blackmore's Enhanced IRC-Notification Collection (BEINC) v4.0
5# Copyright (C) 2013-2018 Simeon Simeonov 5# Copyright (C) 2013-2020 Simeon Simeonov
6 6
7# This program is free software: you can redistribute it and/or modify 7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by 8# it under the terms of the GNU General Public License as published by
@@ -29,26 +29,16 @@ import sys
29 29
30from functools import wraps 30from functools import wraps
31from logging.config import fileConfig 31from logging.config import fileConfig
32 32from http.server import BaseHTTPRequestHandler, HTTPServer
33PY2 = sys.version_info[0] == 2
34PY3 = sys.version_info[0] == 3
35
36if PY3:
37 from http.server import BaseHTTPRequestHandler, HTTPServer
38else:
39 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
40 33
41try: 34try:
42 if PY3: 35 import notify2 as pynotify
43 import notify2 as pynotify 36except ImportError:
44 else:
45 import pynotify
46except ImportError as e:
47 pynotify = None 37 pynotify = None
48 38
49 39
50__author__ = 'Simeon Simeonov' 40__author__ = 'Simeon Simeonov'
51__version__ = '3.0' 41__version__ = '4.0'
52__license__ = 'GPL3' 42__license__ = 'GPL3'
53 43
54 44
@@ -59,28 +49,30 @@ BEINC_CURRENT_CONFIG_VERSION = 3
59 49
60 50
61class BEINCError401(Exception): 51class BEINCError401(Exception):
62 pass 52 """BEINCError401"""
63 53
64 54
65class BEINCError403(Exception): 55class BEINCError403(Exception):
66 pass 56 """BEINCError403"""
67 57
68 58
69class BEINCError404(Exception): 59class BEINCError404(Exception):
70 pass 60 """BEINCError404"""
71 61
72 62
73class BEINCError405(Exception): 63class BEINCError405(Exception):
74 pass 64 """BEINCError405"""
75 65
76 66
77def beinc_login_required(method): 67def eprint(*arg, **kwargs):
78 """ 68 """stdderr print wrapper"""
79 Decorator for checking login credentials 69 print(*arg, file=sys.stderr, flush=True, **kwargs)
80 """ 70
81 71
72def beinc_login_required(method):
73 """Decorator for checking login credentials"""
82 @wraps(method) 74 @wraps(method)
83 def wrapper(self, data, *args, **kwargs): 75 def wrapper(self, data, *arg, **kwargs):
84 if data.get('resource_name') is None: 76 if data.get('resource_name') is None:
85 raise BEINCError403('Resource-name missing') 77 raise BEINCError403('Resource-name missing')
86 if data.get('password') is None: 78 if data.get('password') is None:
@@ -91,122 +83,122 @@ def beinc_login_required(method):
91 raise BEINCError401('Wrong instance or password') 83 raise BEINCError401('Wrong instance or password')
92 if not instance.password_match(data.get('password')): 84 if not instance.password_match(data.get('password')):
93 raise BEINCError401('Wrong instance or password') 85 raise BEINCError401('Wrong instance or password')
94 return method(self, data, *args, **kwargs) 86 return method(self, data, *arg, **kwargs)
95 return wrapper 87 return wrapper
96 88
97 89
98class BEINCInstance(object): 90class BEINCInstance:
99 """ 91 """Represents a single server-instance"""
100 Represents a single server-instance
101 """
102 92
103 def __init__(self, instance_dict): 93 def __init__(self, instance_dict):
104 """ 94 """
105 instance_dict: the config-dictionary node that represents this instance 95 instance_dict: the config-dictionary node that represents this instance
106 """ 96 """
107 self.__message_queue = list() 97 self._message_queue = []
108 self.__osd_type = BEINC_OSD_TYPE_NONE 98 self._osd_type = BEINC_OSD_TYPE_NONE
109 self.__osd_notification = None 99 self._osd_notification = None
110 100
111 self.__name = instance_dict.get('name') 101 self._name = instance_dict.get('name')
112 self.__password = instance_dict.get('password', '') 102 self._password = instance_dict.get('password', '')
113 self.__queue_size = int(instance_dict.get('queue_size', 3)) 103 self._queue_size = int(instance_dict.get('queue_size', 3))
114 if instance_dict['osd_system'].lower() == 'pynotify': 104 if instance_dict['osd_system'].lower() == 'pynotify':
115 self.__queue_size = 0 # disable queueing 105 self._queue_size = 0 # disable queueing
116 if pynotify is None: 106 if pynotify is None:
117 sys.stderr.write( 107 eprint('This server does not possess pynotify capability')
118 'This server does not possess pynotify capability\n') 108 eprint(f'Remove the instance {self._name} or define it with '
119 sys.stderr.write( 109 f'"osd_system": "none" or other '
120 'Remove the instance {0}'.format(self.__name)) 110 f'available backend')
121 sys.stderr.write(
122 'or define it with "osd_system": "none" '
123 'or other available backend\n')
124 sys.exit(errno.EPERM) 111 sys.exit(errno.EPERM)
125 try: 112 try:
126 self.__osd_notification = pynotify.Notification(' ') 113 self._osd_notification = pynotify.Notification(' ')
127 if PY3: 114 self._osd_notification.timeout = 1000 * int(
128 self.__osd_notification.timeout = 1000 * int( 115 instance_dict.get('osd_timeout', 5))
129 instance_dict.get('osd_timeout', 5)) 116 self._osd_notification.set_category('im.received')
130 self.__osd_notification.set_category('im.received') 117 self._osd_type = BEINC_OSD_TYPE_PYNOTIFY
131 else:
132 self.__osd_notification.set_timeout(
133 1000 * int(instance_dict.get('osd_timeout', 5)))
134 self.__osd_notification.set_property(
135 'app_name',
136 '{0} {1}'.format(sys.argv[0], __version__))
137 self.__osd_type = BEINC_OSD_TYPE_PYNOTIFY
138 except Exception as e: 118 except Exception as e:
139 sys.stderr.write( 119 eprint(f'Unable to set up a pynotify notification object '
140 'Unable to set up a ' 120 f'for "{self._name}" ({e})')
141 'pynotify notification object for "{0}" ({1})\n'.format(
142 self.__name,
143 e))
144 sys.exit(errno.EPERM) 121 sys.exit(errno.EPERM)
145 122
146 @property 123 @property
147 def name(self): 124 def name(self):
148 """ 125 """name-property for the server instance (read-only)"""
149 name-property for the server instance (read-only) 126 return self._name
150 """
151 return self.__name
152 127
153 @property 128 @property
154 def queueable(self): 129 def queueable(self):
155 """ 130 """True if this instance has a queueing capability (read-only)"""
156 True if this instance has a queueing capability (read-only) 131 return bool(self._queue_size)
157 """
158 return bool(self.__queue_size)
159 132
160 def password_match(self, password): 133 def password_match(self, password):
161 """ 134 """
162 Returns True if 'passowrd' matches the instance-password, 135 Returns True if 'passowrd' matches the instance-password,
163 otherwise - False 136 otherwise - False
137
138 :param password: The password to compare
139 :type password: str
140
141 :return: True if the password matches, False otherwise
142 :rtype: bool
164 """ 143 """
165 return True if self.__password == password else False 144 return self._password == password
166 145
167 def send_message(self, title, message): 146 def send_message(self, title, message):
168 """ 147 """
169 Displays or enqueues the message, 148 Displays or enqueues the message,
170 depending on the instance's type in regard to the osd_system 149 depending on the instance's type in regard to the osd_system
150
151 :param title: The title
152 :type title: str
153
154 :param message: The message
155 :type message: str
171 """ 156 """
172 if self.__osd_type == BEINC_OSD_TYPE_PYNOTIFY: 157 if self._osd_type == BEINC_OSD_TYPE_PYNOTIFY:
173 self.__send_pynotify_messaage(title, message) 158 self._send_pynotify_messaage(title, message)
174 else: 159 else:
175 self.__send_message_to_queue(title, message) 160 self._send_message_to_queue(title, message)
176 161
177 def get_queue(self): 162 def get_queue(self):
178 """ 163 """Returns a list of dict representation of the message queue"""
179 Returns a list of dict representation of the message queue 164 r_value = self._message_queue
180 """ 165 self._message_queue = []
181 r_value = self.__message_queue
182 self.__message_queue = list()
183 return r_value 166 return r_value
184 167
185 def __send_pynotify_messaage(self, title, message): 168 def _send_pynotify_messaage(self, title, message):
186 """ 169 """
187 Displays pynotify message 170 Displays pynotify message
171
172 :param title: The title
173 :type title: str
174
175 :param message: The message
176 :type message: str
188 """ 177 """
189 self.__osd_notification.update(summary=title, message=message) 178 self._osd_notification.update(summary=title, message=message)
190 self.__osd_notification.show() 179 self._osd_notification.show()
191 180
192 def __send_message_to_queue(self, title, message): 181 def _send_message_to_queue(self, title, message):
193 """ 182 """
194 Enqueues the message 183 Enqueues the message
184
185 :param title: The title
186 :type title: str
187
188 :param message: The message
189 :type message: str
195 """ 190 """
196 if len(self.__message_queue) >= self.__queue_size: 191 if len(self._message_queue) >= self._queue_size:
197 self.__message_queue.pop(0) 192 self._message_queue.pop(0)
198 self.__message_queue.append({'title': title, 'message': message}) 193 self._message_queue.append({'title': title, 'message': message})
199 194
200 195
201class BEINCCustomHandler(BaseHTTPRequestHandler): 196class BEINCCustomHandler(BaseHTTPRequestHandler):
202 """ 197 """Custom handler"""
203 """
204 def do_POST(self): 198 def do_POST(self):
205 """ 199 """Handle POST requests"""
206 Handle POST requests
207 """
208 if self.path.strip('/') not in ('beinc/push', 'beinc/pull'): 200 if self.path.strip('/') not in ('beinc/push', 'beinc/pull'):
209 self.__generate_json_error(404, 'Invalid resource path') 201 self._generate_json_error(404, 'Invalid resource path')
210 return 202 return
211 form = cgi.FieldStorage( 203 form = cgi.FieldStorage(
212 fp=self.rfile, 204 fp=self.rfile,
@@ -222,43 +214,38 @@ class BEINCCustomHandler(BaseHTTPRequestHandler):
222 try: 214 try:
223 result = {} 215 result = {}
224 if self.path.strip('/') == 'beinc/push': 216 if self.path.strip('/') == 'beinc/push':
225 result = self.__handle_push(POST_data) 217 result = self._handle_push(POST_data)
226 elif self.path.strip('/') == 'beinc/pull': 218 elif self.path.strip('/') == 'beinc/pull':
227 result = self.__handle_pull(POST_data) 219 result = self._handle_pull(POST_data)
228 self.__render_to_JSON_response(result) 220 self._render_to_JSON_response(result)
229 except BEINCError401 as e: 221 except BEINCError401 as e:
230 self.__generate_json_error(401, str(e)) 222 self._generate_json_error(401, str(e))
231 except BEINCError403 as e: 223 except BEINCError403 as e:
232 self.__generate_json_error(403, str(e)) 224 self._generate_json_error(403, str(e))
233 except BEINCError404 as e: 225 except BEINCError404 as e:
234 self.__generate_json_error(404, str(e)) 226 self._generate_json_error(404, str(e))
235 except BEINCError405 as e: 227 except BEINCError405 as e:
236 self.__generate_json_error(405, str(e)) 228 self._generate_json_error(405, str(e))
237 except Exception as e: 229 except Exception as e:
238 self.__generate_json_error(500, 230 self._generate_json_error(500, f'Unexpected error: {e}')
239 'Unexpected error: {}'.format(str(e)))
240 231
241 def do_GET(self): 232 def do_GET(self):
242 """ 233 """Handle GET Requests"""
243 Handle GET Requests 234 self._generate_json_error(405, 'Unsupported method')
244 """
245 self.__generate_json_error(405, 'Unsupported method')
246 235
247 @beinc_login_required 236 @beinc_login_required
248 def __handle_push(self, data): 237 def _handle_push(self, data):
249 """ 238 """Handle push"""
250 """
251 instance = self.server.instances[data.get('resource_name')] 239 instance = self.server.instances[data.get('resource_name')]
252 try: 240 try:
253 instance.send_message(data.get('title'), data.get('message')) 241 instance.send_message(data.get('title'), data.get('message'))
254 return {'message': 'OK. Sent.'} 242 return {'message': 'OK. Sent.'}
255 except Exception as e: 243 except Exception as e:
256 self.__generate_json_error(500, str(e)) 244 self._generate_json_error(500, str(e))
257 245
258 @beinc_login_required 246 @beinc_login_required
259 def __handle_pull(self, data): 247 def _handle_pull(self, data):
260 """ 248 """Handle pull"""
261 """
262 instance = self.server.instances[data.get('resource_name')] 249 instance = self.server.instances[data.get('resource_name')]
263 try: 250 try:
264 if not instance.queueable: 251 if not instance.queueable:
@@ -267,13 +254,12 @@ class BEINCCustomHandler(BaseHTTPRequestHandler):
267 return {'message': 'OK. Fetched.', 254 return {'message': 'OK. Fetched.',
268 'data': {'messages': instance.get_queue()}} 255 'data': {'messages': instance.get_queue()}}
269 except Exception as e: 256 except Exception as e:
270 self.__generate_json_error(500, str(e)) 257 self._generate_json_error(500, str(e))
271 258
272 def __generate_json_error(self, code, message): 259 def _generate_json_error(self, code, message):
273 """ 260 """
274 Generates response header and json content for errors 261 Generates response header and json content for errors
275 262
276 Keyword Arguments:
277 :param code: the HTTP code 263 :param code: the HTTP code
278 :type code: int 264 :type code: int
279 265
@@ -288,9 +274,8 @@ class BEINCCustomHandler(BaseHTTPRequestHandler):
288 sort_keys=True, 274 sort_keys=True,
289 indent=4).encode('utf-8')) 275 indent=4).encode('utf-8'))
290 276
291 def __render_to_JSON_response(self, context): 277 def _render_to_JSON_response(self, context):
292 """ 278 """
293 Keyword Arguments:
294 :param context: the context-dict to be converted to json 279 :param context: the context-dict to be converted to json
295 :type context: dict 280 :type context: dict
296 """ 281 """
@@ -305,31 +290,36 @@ class BEINCCustomHandler(BaseHTTPRequestHandler):
305 290
306 291
307class BEINCNotifyServer(HTTPServer): 292class BEINCNotifyServer(HTTPServer):
308 """ 293 """BEINCNotifyServer class"""
309 """ 294 def __init__(self, *arg, **kwargs):
295 """Default constructor"""
296 super().__init__(*arg, **kwargs)
297 self._config = None
298 self._instances = {}
299
310 def set_config(self, config): 300 def set_config(self, config):
311 """ 301 """
312 Sets the configuration dict for the server, instantiates 302 Sets the configuration dict for the server, instantiates
313 the BEINC instances and initiates the defined OSD backends 303 the BEINC instances and initiates the defined OSD backends
314 """ 304 """
315 self.__config = config 305 self._config = config
316 self.__instances = dict()
317 # initialize pynotify if the module exists and if needed 306 # initialize pynotify if the module exists and if needed
318 if pynotify: 307 if pynotify:
319 for instance in self.__config['server']['instances']: 308 for instance in self._config['server']['instances']:
320 # check if we have at least one instance that uses pynotify 309 # check if we have at least one instance that uses pynotify
321 # before initializing it 310 # before initializing it
322 if instance.get('osd_system', '').lower() == 'pynotify': 311 if instance.get('osd_system', '').lower() == 'pynotify':
323 if not pynotify.init('BEINC Notify'): 312 if not pynotify.init('BEINC Notify'):
324 sys.stderr.write('pynotify.init failed! Exiting...\n') 313 eprint('pynotify.init failed! Exiting...')
325 sys.exit(1) 314 sys.exit(1)
326 break 315 break
316 instance = {'name': 'Invalid'}
327 try: 317 try:
328 for instance in self.__config['server']['instances']: 318 for instance in self._config['server']['instances']:
329 self.__instances[instance['name']] = BEINCInstance(instance) 319 self._instances[instance['name']] = BEINCInstance(instance)
330 logger.info('Instance "{0}" added'.format(instance['name'])) 320 logger.info('Instance %s added', instance['name'])
331 except Exception as e: 321 except Exception as e:
332 sys.stderr.write('Unable to create instance "{0}": {1}\n'.format( 322 eprint('Unable to create instance "{0}": {1}'.format(
333 instance['name'], 323 instance['name'],
334 e)) 324 e))
335 sys.exit(1) 325 sys.exit(1)
@@ -339,7 +329,7 @@ class BEINCNotifyServer(HTTPServer):
339 """ 329 """
340 a property that returns the instance list (read-only) 330 a property that returns the instance list (read-only)
341 """ 331 """
342 return self.__instances 332 return self._instances
343 333
344 334
345if __name__ == '__main__': 335if __name__ == '__main__':
@@ -390,15 +380,14 @@ if __name__ == '__main__':
390 parser.add_argument( 380 parser.add_argument(
391 '-v', '--version', 381 '-v', '--version',
392 action='version', 382 action='version',
393 version='%(prog)s {0}'.format(__version__), 383 version=f'%(prog)s {__version__}',
394 help='Display program-version and exit') 384 help='Display program-version and exit')
395 args = parser.parse_args() 385 args = parser.parse_args()
396 try: 386 try:
397 with open(args.config_file, 'r') as fp: 387 with open(args.config_file, 'r') as fp:
398 config_dict = json.load(fp) 388 config_dict = json.load(fp)
399 except Exception as e: 389 except Exception as e:
400 sys.stderr.write('Unable to parse {0}: {1}\n'.format(args.config_file, 390 eprint(f'Unable to parse {args.config_file}: {e}')
401 e))
402 sys.exit(errno.EIO) 391 sys.exit(errno.EIO)
403 try: 392 try:
404 if os.path.isfile(args.logger_config): 393 if os.path.isfile(args.logger_config):
@@ -411,11 +400,11 @@ if __name__ == '__main__':
411 logger = logging.getLogger('beinc') 400 logger = logging.getLogger('beinc')
412 logger.info('BEINC starting. Loading config...') 401 logger.info('BEINC starting. Loading config...')
413 if config_dict.get('config_version') != BEINC_CURRENT_CONFIG_VERSION: 402 if config_dict.get('config_version') != BEINC_CURRENT_CONFIG_VERSION:
414 sys.stderr.write( 403 eprint(
415 'WARNING: The version of the config-file: {0} ({1}) ' 404 'WARNING: The version of the config-file: {0} ({1}) '
416 'does not correspond to the latest version supported ' 405 'does not correspond to the latest version supported '
417 'by this program ({2})\nCheck beinc_config_sample.json ' 406 'by this program ({2})\nCheck beinc_config_sample.json '
418 'for the newest features!\n'.format( 407 'for the newest features!'.format(
419 args.config_file, 408 args.config_file,
420 config_dict.get('config_version', 'Not set'), 409 config_dict.get('config_version', 'Not set'),
421 BEINC_CURRENT_CONFIG_VERSION)) 410 BEINC_CURRENT_CONFIG_VERSION))
@@ -441,6 +430,6 @@ if __name__ == '__main__':
441 except KeyboardInterrupt: 430 except KeyboardInterrupt:
442 print('\n\nTerminating...') 431 print('\n\nTerminating...')
443 except Exception as e: 432 except Exception as e:
444 sys.stderr.write('BEINCServer critical error: {0}\n'.format(e)) 433 eprint(f'BEINCServer critical error: {e}')
445 sys.exit(1) 434 sys.exit(1)
446 sys.exit(0) 435 sys.exit(0)
diff --git a/beinc_weechat.py b/beinc_weechat.py
index 132a22a..c1a1e1e 100644
--- a/beinc_weechat.py
+++ b/beinc_weechat.py
@@ -1,7 +1,7 @@
1# -*- coding: utf-8 -*- 1# -*- coding: utf-8 -*-
2 2
3# Blackmore's Enhanced IRC-Notification Collection (BEINC) v3.0 3# Blackmore's Enhanced IRC-Notification Collection (BEINC) v4.0
4# Copyright (C) 2013-2019 Simeon Simeonov 4# Copyright (C) 2013-2020 Simeon Simeonov
5 5
6# This program is free software: you can redistribute it and/or modify 6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by 7# it under the terms of the GNU General Public License as published by
@@ -15,35 +15,25 @@
15 15
16# You should have received a copy of the GNU General Public License 16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>. 17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18"""BEINC client for Weechat"""
19
20import datetime 19import datetime
21import json 20import json
22import os 21import os
23import socket 22import socket
24import ssl 23import ssl
25import sys 24import urllib.parse
25import urllib.request
26 26
27import weechat 27import weechat
28 28
29PY2 = sys.version_info[0] == 2
30PY3 = sys.version_info[0] == 3
31
32if PY3:
33 from urllib.parse import urlencode
34 from urllib.request import urlopen
35else:
36 from urllib import urlencode
37 from urllib2 import urlopen
38
39 29
40__author__ = 'Simeon Simeonov' 30__author__ = 'Simeon Simeonov'
41__version__ = '3.0' 31__version__ = '4.0'
42__license__ = 'GPL3' 32__license__ = 'GPL3'
43 33
44 34
45enabled = True 35enabled = True
46global_values = dict() 36global_values = {}
47 37
48# few constants # 38# few constants #
49BEINC_POLICY_NONE = 0 39BEINC_POLICY_NONE = 0
@@ -52,322 +42,317 @@ BEINC_POLICY_LIST_ONLY = 2
52BEINC_CURRENT_CONFIG_VERSION = 2 42BEINC_CURRENT_CONFIG_VERSION = 2
53 43
54 44
55class WeechatTarget(object): 45class WeechatTarget:
56 """ 46 """
57 The target (destination) class 47 The target (destination) class
48
58 Each remote destination is represented as a WeechatTarget object 49 Each remote destination is represented as a WeechatTarget object
59 """ 50 """
60 51
61 def __init__(self, target_dict): 52 def __init__(self, target_dict):
62 """ 53 """
63 target_dict: the config-dictionary node that represents this instance 54 :param target_dict: The config-dict node that represents this instance
55 :type target_dict: dict
64 """ 56 """
65 self.__name = target_dict.get('name', '') 57 self._name = target_dict.get('name', '')
66 if self.__name == '': 58 if self._name == '':
67 raise Exception('"name" not defined for target') 59 raise Exception('"name" not defined for target')
68 self.__url = target_dict.get('target_url', '') 60 self._url = target_dict.get('target_url', '')
69 if self.__url == '': 61 if self._url == '':
70 raise Exception('"target_url" not defined for target') 62 raise Exception('"target_url" not defined for target')
71 self.__password = target_dict.get('target_password', '') 63 self._password = target_dict.get('target_password', '')
72 self.__pm_title_template = target_dict.get('pm_title_template', 64 self._pm_title_template = target_dict.get('pm_title_template',
73 '%s @ %S') 65 '%s @ %S')
74 self.__pm_message_template = target_dict.get('pm_message_template', 66 self._pm_message_template = target_dict.get('pm_message_template',
75 '%m') 67 '%m')
76 self.__cm_title_template = target_dict.get('cm_title_template', 68 self._cm_title_template = target_dict.get('cm_title_template',
77 '%c @ %S') 69 '%c @ %S')
78 self.__cm_message_template = target_dict.get('cm_message_template', 70 self._cm_message_template = target_dict.get('cm_message_template',
79 '%s -> %m') 71 '%s -> %m')
80 self.__nm_title_template = target_dict.get('nm_title_template', 72 self._nm_title_template = target_dict.get('nm_title_template',
81 '%c @ %S') 73 '%c @ %S')
82 self.__nm_message_template = target_dict.get('nm_message_template', 74 self._nm_message_template = target_dict.get('nm_message_template',
83 '%s -> %m') 75 '%s -> %m')
84 self.__chans = set(target_dict.get('channel_list', list())) 76 self._chans = set(target_dict.get('channel_list', []))
85 self.__nicks = set(target_dict.get('nick_list', list())) 77 self._nicks = set(target_dict.get('nick_list', []))
86 self.__chan_messages_policy = int(target_dict.get( 78 self._chan_messages_policy = int(target_dict.get(
87 'channel_messages_policy', 79 'channel_messages_policy',
88 BEINC_POLICY_LIST_ONLY)) 80 BEINC_POLICY_LIST_ONLY))
89 self.__priv_messages_policy = int(target_dict.get( 81 self._priv_messages_policy = int(target_dict.get(
90 'private_messages_policy', 82 'private_messages_policy',
91 BEINC_POLICY_ALL)) 83 BEINC_POLICY_ALL))
92 self.__notifications_policy = int(target_dict.get( 84 self._notifications_policy = int(target_dict.get(
93 'notifications_policy', 85 'notifications_policy',
94 BEINC_POLICY_ALL)) 86 BEINC_POLICY_ALL))
95 self.__cert_file = target_dict.get('target_cert_file') 87 self._cert_file = target_dict.get('target_cert_file')
96 self.__timestamp_format = target_dict.get('target_timestamp_format', 88 self._timestamp_format = target_dict.get('target_timestamp_format',
97 '%H:%M:%S') 89 '%H:%M:%S')
98 self.__debug = bool(target_dict.get('debug', False)) 90 self._debug = bool(target_dict.get('debug', False))
99 self.__enabled = bool(target_dict.get('enabled', True)) 91 self._enabled = bool(target_dict.get('enabled', True))
100 self.__socket_timeout = int(target_dict.get('socket_timeout', 3)) 92 self._socket_timeout = int(target_dict.get('socket_timeout', 3))
101 self.__ssl_ciphers = target_dict.get('ssl_ciphers', '') 93 self._ssl_ciphers = target_dict.get('ssl_ciphers', '')
102 self.__disable_hostname_check = bool( 94 self._disable_hostname_check = bool(
103 target_dict.get('disable-hostname-check', False)) 95 target_dict.get('disable-hostname-check', False))
104 self.__ssl_version = target_dict.get('ssl_version', 'auto') 96 self._ssl_version = target_dict.get('ssl_version', 'auto')
105 self.__last_message = None # datetime.datetime instance 97 self._last_message = None # datetime.datetime instance
106 self.__context = None 98 self._context = None
107 self.__context_setup() 99 self._context_setup()
108 100
109 @property 101 @property
110 def name(self): 102 def name(self):
111 """ 103 """Target name (read-only property)"""
112 Target name (read-only property) 104 return self._name
113 """
114 return self.__name
115 105
116 @property 106 @property
117 def chans(self): 107 def chans(self):
118 """ 108 """Target channel list (read-only property)"""
119 Target channel list (read-only property) 109 return self._chans
120 """
121 return self.__chans
122 110
123 @property 111 @property
124 def nicks(self): 112 def nicks(self):
125 """ 113 """Target nick list (read-only property)"""
126 Target nick list (read-only property) 114 return self._nicks
127 """
128 return self.__nicks
129 115
130 @property 116 @property
131 def channel_messages_policy(self): 117 def channel_messages_policy(self):
132 """ 118 """The target's channel messages policy (read-only property)"""
133 The target's channel messages policy (read-only property) 119 return self._chan_messages_policy
134 """
135 return self.__chan_messages_policy
136 120
137 @property 121 @property
138 def private_messages_policy(self): 122 def private_messages_policy(self):
139 """ 123 """The target's private messages policy (read-only property)"""
140 The target's private messages policy (read-only property) 124 return self._priv_messages_policy
141 """
142 return self.__priv_messages_policy
143 125
144 @property 126 @property
145 def notifications_policy(self): 127 def notifications_policy(self):
146 """ 128 """The target's notifications policy (read-only property)"""
147 The target's notifications policy (read-only property) 129 return self._notifications_policy
148 """
149 return self.__notifications_policy
150 130
151 @property 131 @property
152 def enabled(self): 132 def enabled(self):
153 """ 133 """The target's enabled status (bool property)"""
154 The target's enabled status (bool property) 134 return self._enabled
155 """
156 return self.__enabled
157 135
158 @enabled.setter 136 @enabled.setter
159 def enabled(self, value): 137 def enabled(self, value):
160 """ 138 """The target's enabled status (bool property)"""
161 The target's enabled status (bool property) 139 self._enabled = value
162 """
163 self.__enabled = value
164 140
165 def __repr__(self): 141 def __repr__(self):
166 """ 142 """repr() implementation"""
167 """
168 last_message = 'never' 143 last_message = 'never'
169 if self.__last_message is not None: 144 if self._last_message is not None:
170 last_message = self.__last_message.strftime('%Y-%m-%d %H:%M:%S') 145 last_message = self._last_message.strftime('%Y-%m-%d %H:%M:%S')
171 return ('name: {0}\nurl: {1}\nenabled: {2}\nchannel_list: {3}\n' 146 return ('name: {0}\nurl: {1}\nenabled: {2}\nchannel_list: {3}\n'
172 'nick_list: {4}\nchannel_messages_policy: {5}\n' 147 'nick_list: {4}\nchannel_messages_policy: {5}\n'
173 'private_messages_policy: {6}\nnotifications_policy: {7}\n' 148 'private_messages_policy: {6}\nnotifications_policy: {7}\n'
174 'last message: {8}\nsocket timeout: {9}\nssl-version: {10}\n' 149 'last message: {8}\nsocket timeout: {9}\nssl-version: {10}\n'
175 'ciphers: {11}\ndisable hostname check: {12}\n' 150 'ciphers: {11}\ndisable hostname check: {12}\n'
176 'debug: {13}\n\n'.format( 151 'debug: {13}\n\n'.format(
177 self.__name, 152 self._name,
178 self.__url, 153 self._url,
179 'yes' if self.__enabled else 'no', 154 'yes' if self._enabled else 'no',
180 ', '.join(self.__chans), 155 ', '.join(self._chans),
181 ', '.join(self.__nicks), 156 ', '.join(self._nicks),
182 self.__chan_messages_policy, 157 self._chan_messages_policy,
183 self.__priv_messages_policy, 158 self._priv_messages_policy,
184 self.__notifications_policy, 159 self._notifications_policy,
185 last_message, 160 last_message,
186 self.__socket_timeout, 161 self._socket_timeout,
187 self.__ssl_version, 162 self._ssl_version,
188 self.__ssl_ciphers or 'auto', 163 self._ssl_ciphers or 'auto',
189 'yes' if self.__disable_hostname_check else 'no', 164 'yes' if self._disable_hostname_check else 'no',
190 'yes' if self.__debug else 'no')) 165 'yes' if self._debug else 'no'))
191 166
192 def send_private_message_notification(self, values): 167 def send_private_message_notification(self, values):
193 """ 168 """
194 sends a private message notification to the represented target 169 Sends a private message notification to the represented target
195 170
196 values: dict pupulated by the irc msg-handler 171 :param value: Dict pupulated by the irc msg-handler
172 :type value: dict
197 """ 173 """
198 try: 174 try:
199 title = self.__fetch_formatted_str(self.__pm_title_template, 175 title = self._fetch_formatted_str(self._pm_title_template,
200 values) 176 values)
201 message = self.__fetch_formatted_str( 177 message = self._fetch_formatted_str(
202 self.__pm_message_template, 178 self._pm_message_template,
203 values) 179 values)
204 if not self.__send_beinc_message(title, message) and self.__debug: 180 if not self._send_beinc_message(title, message) and self._debug:
205 beinc_prnt( 181 beinc_prnt(
206 'BEINC DEBUG: send_private_message_notification-ERROR ' 182 f'BEINC DEBUG: send_private_message_notification-ERROR '
207 'for "{0}": __send_beinc_message -> False'.format( 183 f'for "{self._name}": _send_beinc_message -> False')
208 self.__name))
209 except Exception as e: 184 except Exception as e:
210 if self.__debug: 185 if self._debug:
211 beinc_prnt( 186 beinc_prnt(
212 'BEINC DEBUG: send_private_message_notification-ERROR ' 187 f'BEINC DEBUG: send_private_message_notification-ERROR '
213 'for "{0}": {1}'.format(self.__name, e)) 188 f'for "{self._name}": {e}')
214 189
215 def send_channel_message_notification(self, values): 190 def send_channel_message_notification(self, values):
216 """ 191 """
217 sends a channel message notification to the represented target 192 Sends a channel message notification to the represented target
218 193
219 values: dict pupulated by the irc msg-handler 194 :param value: Dict pupulated by the irc msg-handler
195 :type value: dict
220 """ 196 """
221 try: 197 try:
222 title = self.__fetch_formatted_str(self.__cm_title_template, 198 title = self._fetch_formatted_str(self._cm_title_template,
223 values) 199 values)
224 message = self.__fetch_formatted_str(self.__cm_message_template, 200 message = self._fetch_formatted_str(self._cm_message_template,
225 values) 201 values)
226 if not self.__send_beinc_message(title, message) and self.__debug: 202 if not self._send_beinc_message(title, message) and self._debug:
227 beinc_prnt( 203 beinc_prnt(
228 'BEINC DEBUG: send_channel_message_notification-ERROR ' 204 f'BEINC DEBUG: send_channel_message_notification-ERROR '
229 'for "{0}": __send_beinc_message -> False'.format( 205 f'for "{self._name}": _send_beinc_message -> False')
230 self.__name))
231 except Exception as e: 206 except Exception as e:
232 if self.__debug: 207 if self._debug:
233 beinc_prnt( 208 beinc_prnt(
234 'BEINC DEBUG: send_channel_message_notification-ERROR ' 209 f'BEINC DEBUG: send_channel_message_notification-ERROR '
235 'for "{0}": {1}'.format(self.__name, e)) 210 f'for "{self._name}": {e}')
236 211
237 def send_notify_message_notification(self, values): 212 def send_notify_message_notification(self, values):
238 """ 213 """
239 sends a notify message notification to the represented target 214 Sends a notify message notification to the represented target
240 215
241 values: dict pupulated by the irc msg-handler 216 :param value: Dict pupulated by the irc msg-handler
217 :type value: dict
242 """ 218 """
243 try: 219 try:
244 title = self.__fetch_formatted_str(self.__nm_title_template, 220 title = self._fetch_formatted_str(self._nm_title_template,
245 values) 221 values)
246 message = self.__fetch_formatted_str(self.__nm_message_template, 222 message = self._fetch_formatted_str(self._nm_message_template,
247 values) 223 values)
248 if not self.__send_beinc_message(title, message) and self.__debug: 224 if not self._send_beinc_message(title, message) and self._debug:
249 beinc_prnt( 225 beinc_prnt(
250 'BEINC DEBUG: send_notify_message_notification-ERROR ' 226 f'BEINC DEBUG: send_notify_message_notification-ERROR '
251 'for "{0}": __send_beinc_message -> False'.format( 227 f'for "{self._name}": _send_beinc_message -> False')
252 self.__name))
253 except Exception as e: 228 except Exception as e:
254 if self.__debug: 229 if self._debug:
255 beinc_prnt( 230 beinc_prnt(
256 'BEINC DEBUG: send_notify_message_notification-ERROR ' 231 f'BEINC DEBUG: send_notify_message_notification-ERROR '
257 'for "{0}": {1}'.format(self.__name, e)) 232 f'for "{self._name}": {e}')
258 233
259 def send_broadcast_notification(self, message): 234 def send_broadcast_notification(self, message):
260 """ 235 """
261 sends a 'pure' broadcast / test message notification 236 Sends a 'pure' broadcast / test message notification
262 to the represented target 237 to the represented target
263 238
264 message: a single message string 239 :param message: A single message string
240 :type message: str
265 """ 241 """
266 try: 242 try:
267 title = 'BEINC broadcast' 243 title = 'BEINC broadcast'
268 if not self.__send_beinc_message(title, message) and self.__debug: 244 if not self._send_beinc_message(title, message) and self._debug:
269 beinc_prnt( 245 beinc_prnt(
270 'BEINC DEBUG: send_broadcast_notification-ERROR ' 246 f'BEINC DEBUG: send_broadcast_notification-ERROR '
271 'for "{0}": __send_beinc_message -> False'.format( 247 f'for "{self._name}": _send_beinc_message -> False')
272 self.__name))
273 except Exception as e: 248 except Exception as e:
274 if self.__debug: 249 if self._debug:
275 beinc_prnt( 250 beinc_prnt(
276 'BEINC DEBUG: send_broadcast_notification-ERROR ' 251 f'BEINC DEBUG: send_broadcast_notification-ERROR '
277 'for "{0}": {1}'.format(self.__name, e)) 252 f'for "{self._name}": {e}')
278 253
279 def __context_setup(self): 254 def _context_setup(self):
280 """ 255 """Sets up the SSL context"""
281 """ 256 if self._context is not None:
282 if self.__context is not None:
283 return True 257 return True
284 try: 258 try:
285 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 259 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
286 context.verify_mode = ssl.CERT_NONE 260 context.verify_mode = ssl.CERT_NONE
287 if self.__cert_file: 261 if self._cert_file:
288 context.verify_mode = ssl.CERT_REQUIRED 262 context.verify_mode = ssl.CERT_REQUIRED
289 context.load_verify_locations(cafile=os.path.expanduser( 263 context.load_verify_locations(cafile=os.path.expanduser(
290 self.__cert_file)) 264 self._cert_file))
291 context.check_hostname = bool( 265 context.check_hostname = bool(
292 not self.__disable_hostname_check) 266 not self._disable_hostname_check)
293 if self.__ssl_ciphers and self.__ssl_ciphers != 'auto': 267 if self._ssl_ciphers and self._ssl_ciphers != 'auto':
294 context.set_ciphers(self.__ssl_ciphers) 268 context.set_ciphers(self._ssl_ciphers)
295 self.__context = context 269 self._context = context
296 return True 270 return True
297 except ssl.SSLError as e: 271 except ssl.SSLError as e:
298 if self.__debug: 272 if self._debug:
299 beinc_prnt('BEINC DEBUG: SSL/TLS error: {0}\n'.format(e)) 273 beinc_prnt(f'BEINC DEBUG: SSL/TLS error: {e}\n')
300 except Exception as e: 274 except Exception as e:
301 if self.__debug: 275 if self._debug:
302 beinc_prnt('BEINC DEBUG: Generic context error: {0}\n'.format( 276 beinc_prnt(f'BEINC DEBUG: Generic context error: {e}\n')
303 e)) 277 self._context = None
304 self.__context = None
305 return False 278 return False
306 279
307 def __fetch_formatted_str(self, template, values): 280 def _fetch_formatted_str(self, template, values):
308 """ 281 """
309 returns a formatted string by replacing the defined 282 Returns a formatted string by replacing the defined
310 macros in 'template' the the corresponding values from 'values' 283 macros in 'template' the the corresponding values from 'values'
311 284
312 values: dict 285 :param template: The template to use
313 template: str 286 :type template: str
287
288 :param values: The values dict
289 :type values: dict
290
291 :return: The formatted string
292 :rtype: str
314 """ 293 """
315 timestamp = datetime.datetime.now().strftime(self.__timestamp_format) 294 timestamp = datetime.datetime.now().strftime(self._timestamp_format)
316 replacements = {'%S': values['server'], 295 replacements = {'%S': values['server'],
317 '%s': values['source_nick'], 296 '%s': values['source_nick'],
318 '%c': values['channel'], 297 '%c': values['channel'],
319 '%m': values['message'], 298 '%m': values['message'],
320 '%t': timestamp, 299 '%t': timestamp,
321 '%p': u'BEINC', 300 '%p': 'BEINC',
322 '%n': values['own_nick']} 301 '%n': values['own_nick']}
323 for key, value in replacements.items(): 302 for key, value in replacements.items():
324 template = template.replace(key, value) 303 template = template.replace(key, value)
325 return template.encode('utf-8') 304 return template
326 305
327 def __send_beinc_message(self, title, message): 306 def _send_beinc_message(self, title, message):
328 """ 307 """
329 the function implements the BEINC "protocol" by generating a simple 308 The method implements the BEINC "protocol" by generating a simple
330 POST request 309 POST request
310
311 :param title: The title
312 :type title: str
313
314 :param message: The message
315 :type message: str
316
317 :return: The status
318 :rtype: bool
331 """ 319 """
332 try: 320 try:
333 if self.__context is None and not self.__context_setup(): 321 if self._context is None and not self._context_setup():
334 return False 322 return False
335 response = urlopen( 323 response = urllib.request.urlopen(
336 self.__url, 324 self._url,
337 data=urlencode( 325 data=urllib.parse.urlencode(
338 ( 326 (
339 ('resource_name', self.__name), 327 ('resource_name', self._name),
340 ('password', self.__password), 328 ('password', self._password),
341 ('title', title), 329 ('title', title),
342 ('message', message) 330 ('message', message)
343 )).encode('utf-8'), 331 )).encode('utf-8'),
344 timeout=self.__socket_timeout, 332 timeout=self._socket_timeout,
345 context=self.__context) 333 context=self._context)
346 response_dict = json.loads(response.read().decode('utf-8')) 334 response_dict = json.loads(response.read().decode('utf-8'))
347 if response.code != 200: 335 if response.code != 200:
348 raise socket.error(response_dict.get('message', '')) 336 raise socket.error(response_dict.get('message', ''))
349 if self.__debug: 337 if self._debug:
350 beinc_prnt('BEINC DEBUG: Server responded: {0}'.format( 338 beinc_prnt('BEINC DEBUG: Server responded: {0}'.format(
351 response_dict.get('message'))) 339 response_dict.get('message')))
352 self.__last_message = datetime.datetime.now() 340 self._last_message = datetime.datetime.now()
353 return True 341 return True
354 except ssl.SSLError as e: 342 except ssl.SSLError as e:
355 if self.__debug: 343 if self._debug:
356 beinc_prnt('BEINC DEBUG: SSL/TLS error: {0}\n'.format(e)) 344 beinc_prnt(f'BEINC DEBUG: SSL/TLS error: {e}\n')
357 except socket.error as e: 345 except socket.error as e:
358 if self.__debug: 346 if self._debug:
359 beinc_prnt('BEINC DEBUG: Connection error: {0}\n'.format(e)) 347 beinc_prnt(f'BEINC DEBUG: Connection error: {e}\n')
360 except Exception as e: 348 except Exception as e:
361 if self.__debug: 349 if self._debug:
362 beinc_prnt('BEINC DEBUG: Unable to send message: {0}\n'.format( 350 beinc_prnt(f'BEINC DEBUG: Unable to send message: {e}\n')
363 e))
364 return False 351 return False
365 352
366 353
367def beinc_prnt(message_str): 354def beinc_prnt(message_str):
368 """ 355 """wrapper around weechat.prnt"""
369 wrapper around weechat.prnt
370 """
371 if global_values['use_current_buffer']: 356 if global_values['use_current_buffer']:
372 weechat.prnt(weechat.current_buffer(), message_str) 357 weechat.prnt(weechat.current_buffer(), message_str)
373 else: 358 else:
@@ -375,9 +360,7 @@ def beinc_prnt(message_str):
375 360
376 361
377def beinc_cmd_broadcast_handler(cmd_tokens): 362def beinc_cmd_broadcast_handler(cmd_tokens):
378 """ 363 """handles: '/beinc broadcast' command actions"""
379 handles: '/beinc broadcast' command actions
380 """
381 if not cmd_tokens: 364 if not cmd_tokens:
382 beinc_prnt('beinc broadcast <message>') 365 beinc_prnt('beinc broadcast <message>')
383 return weechat.WEECHAT_RC_OK 366 return weechat.WEECHAT_RC_OK
@@ -388,9 +371,7 @@ def beinc_cmd_broadcast_handler(cmd_tokens):
388 371
389 372
390def beinc_cmd_target_handler(cmd_tokens): 373def beinc_cmd_target_handler(cmd_tokens):
391 """ 374 """handles: '/beinc target' command actions"""
392 handles: '/beinc target' command actions
393 """
394 if not cmd_tokens or cmd_tokens[0] not in ['list', 'enable', 'disable']: 375 if not cmd_tokens or cmd_tokens[0] not in ['list', 'enable', 'disable']:
395 beinc_prnt('beinc target [ list | enable <name> | disable <name> ]') 376 beinc_prnt('beinc target [ list | enable <name> | disable <name> ]')
396 return weechat.WEECHAT_RC_OK 377 return weechat.WEECHAT_RC_OK
@@ -410,10 +391,10 @@ def beinc_cmd_target_handler(cmd_tokens):
410 for target in target_list: 391 for target in target_list:
411 if target.name == name: 392 if target.name == name:
412 target.enabled = True 393 target.enabled = True
413 beinc_prnt('target "{0}" enabled'.format(name)) 394 beinc_prnt(f'target "{name}" enabled')
414 break 395 break
415 else: 396 else:
416 beinc_prnt('no matching target for "{0}"'.format(name)) 397 beinc_prnt(f'no matching target for "{name}"')
417 elif cmd_tokens[0] == 'disable': 398 elif cmd_tokens[0] == 'disable':
418 if not cmd_tokens[1:]: 399 if not cmd_tokens[1:]:
419 beinc_prnt('missing a name-argument') 400 beinc_prnt('missing a name-argument')
@@ -422,17 +403,15 @@ def beinc_cmd_target_handler(cmd_tokens):
422 for target in target_list: 403 for target in target_list:
423 if target.name == name: 404 if target.name == name:
424 target.enabled = False 405 target.enabled = False
425 beinc_prnt('target "{0}" disabled'.format(name)) 406 beinc_prnt(f'target "{name}" disabled')
426 break 407 break
427 else: 408 else:
428 beinc_prnt('no matching target for "{0}"'.format(name)) 409 beinc_prnt(f'no matching target for "{name}"')
429 return weechat.WEECHAT_RC_OK 410 return weechat.WEECHAT_RC_OK
430 411
431 412
432def beinc_command(data, buffer_obj, args): 413def beinc_command(data, buffer_obj, args):
433 """ 414 """Callback function handling the Weechat's /beinc command"""
434 Callback function handling the Weechat's /beinc command
435 """
436 global enabled 415 global enabled
437 cmd_tokens = args.split() 416 cmd_tokens = args.split()
438 if not cmd_tokens: 417 if not cmd_tokens:
@@ -457,15 +436,13 @@ def beinc_command(data, buffer_obj, args):
457 436
458 437
459def beinc_privmsg_handler(data, signal, signal_data): 438def beinc_privmsg_handler(data, signal, signal_data):
460 """ 439 """Callback function the *PRIVMSG* IRC messages hooked by Weechat"""
461 Callback function the *PRIVMSG* IRC messages hooked by Weechat
462 """
463 if not enabled: 440 if not enabled:
464 return weechat.WEECHAT_RC_OK 441 return weechat.WEECHAT_RC_OK
465 prvmsg_dict = weechat.info_get_hashtable('irc_message_parse', 442 prvmsg_dict = weechat.info_get_hashtable('irc_message_parse',
466 {'message': signal_data}) 443 {'message': signal_data})
467 # packing the privmsg handler values 444 # packing the privmsg handler values
468 ph_values = dict() 445 ph_values = {}
469 ph_values['server'] = signal.split(',')[0] 446 ph_values['server'] = signal.split(',')[0]
470 ph_values['own_nick'] = weechat.info_get('irc_nick', ph_values['server']) 447 ph_values['own_nick'] = weechat.info_get('irc_nick', ph_values['server'])
471 ph_values['channel'] = prvmsg_dict['arguments'].split(':')[0].strip() 448 ph_values['channel'] = prvmsg_dict['arguments'].split(':')[0].strip()
@@ -529,8 +506,8 @@ def beinc_init():
529 global global_values 506 global global_values
530 507
531 # global chans/nicks sets are used to speed up the filtering 508 # global chans/nicks sets are used to speed up the filtering
532 global_values = dict() 509 global_values = {}
533 target_list = list() 510 target_list = []
534 custom_error = '' 511 custom_error = ''
535 global_values['global_channel_messages_policy'] = False 512 global_values['global_channel_messages_policy'] = False
536 global_values['global_private_messages_policy'] = False 513 global_values['global_private_messages_policy'] = False
@@ -541,7 +518,7 @@ def beinc_init():
541 beinc_config_file_str = os.path.join( 518 beinc_config_file_str = os.path.join(
542 weechat.info_get('weechat_dir', ''), 519 weechat.info_get('weechat_dir', ''),
543 'beinc_weechat.json') 520 'beinc_weechat.json')
544 beinc_prnt('Parsing {0}...'.format(beinc_config_file_str)) 521 beinc_prnt(f'Parsing {beinc_config_file_str}...')
545 custom_error = 'load error' 522 custom_error = 'load error'
546 with open(beinc_config_file_str, 'r') as fp: 523 with open(beinc_config_file_str, 'r') as fp:
547 config_dict = json.load(fp, encoding='utf-8') 524 config_dict = json.load(fp, encoding='utf-8')
@@ -562,7 +539,7 @@ def beinc_init():
562 try: 539 try:
563 new_target = WeechatTarget(target) 540 new_target = WeechatTarget(target)
564 except Exception as e: 541 except Exception as e:
565 beinc_prnt('Unable to add target: {0}'.format(e)) 542 beinc_prnt(f'Unable to add target: {e}')
566 continue 543 continue
567 if new_target.channel_messages_policy: 544 if new_target.channel_messages_policy:
568 global_values['global_channel_messages_policy'] = True 545 global_values['global_channel_messages_policy'] = True
@@ -571,12 +548,11 @@ def beinc_init():
571 if new_target.notifications_policy: 548 if new_target.notifications_policy:
572 global_values['global_notifications_policy'] = True 549 global_values['global_notifications_policy'] = True
573 target_list.append(new_target) 550 target_list.append(new_target)
574 beinc_prnt('BEINC target "{0}" added'.format(new_target.name)) 551 beinc_prnt(f'BEINC target "{new_target.name}" added')
575 beinc_prnt('Done!') 552 beinc_prnt('Done!')
576 except Exception as e: 553 except Exception as e:
577 beinc_prnt('ERROR: unable to parse {0}: {1} - {2}\n' 554 beinc_prnt(f'ERROR: unable to parse {beinc_config_file_str}: '
578 'BEINC is now disabled'.format( 555 f'{custom_error} - {e}\nBEINC is now disabled')
579 beinc_config_file_str, custom_error, e))
580 enabled = False 556 enabled = False
581 # do not return error / exit the script 557 # do not return error / exit the script
582 # in order to give a smoother opportunity to fix a 'broken' config 558 # in order to give a smoother opportunity to fix a 'broken' config