diff --git a/jabber.py b/jabber.py new file mode 100644 index 0000000..838778f --- /dev/null +++ b/jabber.py @@ -0,0 +1,2263 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2013 Sebastien Helleu +# Copyright (C) 2010 xt +# Copyright (C) 2010 Aleksey V. Zapparov +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +""" +Jabber/XMPP protocol for WeeChat. +(this script requires WeeChat 0.3.0 (or newer) and xmpppy library) + +This will use the value of the environment variable https_proxy +(lowercase with the s) to set an HTTP proxy. + +For help, see /help jabber + +/jabber list + add + | [[:]] + connect|disconnect|del [] + alias [add|del ] + away [] + buddies + priority [] + status [] + presence | [online|chat|away|xa|dnd] + +""" +# History: +# + +SCRIPT_NAME = "jabber" +SCRIPT_AUTHOR = "Sebastien Helleu " +SCRIPT_VERSION = "2.0" +SCRIPT_LICENSE = "GPL3" +SCRIPT_DESC = "Jabber/XMPP protocol for WeeChat" +SCRIPT_COMMAND = SCRIPT_NAME + +import os +import re +import traceback +import warnings + +try: + import weechat as w + weechat = w + import_ok = True +except: + print("This script must be run under WeeChat.") + print("Get WeeChat now at: http://www.weechat.org/") + import_ok = False + +# On import, xmpp may produce warnings about using hashlib instead of +# deprecated sha and md5. Since the code producing those warnings is +# outside this script, catch them and ignore. +original_filters = warnings.filters[:] +warnings.filterwarnings("ignore", category=DeprecationWarning) +try: + import xmpp +except: + print("Package python-xmpp (xmpppy) must be installed to use Xmpp protocol.") + print("Get xmpppy with your package manager, or at this URL: http://xmpppy.sourceforge.net/") + import_ok = False +finally: + warnings.filters = original_filters + +def LOG_debug(what, level): + if jabber_debug_enabled(): + w_prnt('', what) + +def w_prnt(where, what='', *args): + if args: + w.prnt('', w.prefix('!') + str(repr(args))) + elif not what: + w.prnt('', w.prefix('%') + str(where)) + elif type(what) != str or type(where) != str: + w.prnt('', w.prefix('$') + str(where) + str(what)) + else: + try: + w.prnt(where, what) + except Exception as e: + w.prnt('', 'w_prnt: ' + str(e)) + +# ==============================[ global vars ]=============================== + +jabber_servers = [] +jabber_server_options = { + "jid" : { "type" : "string", + "desc": "xmpp id (user@server.tld)", + "min": 0, + "max": 0, + "string_values": "", + "default": "", + "value": "", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "priority": { "type": "integer", + "desc": "Default resource priority", + "min": 0, + "max": 65535, + "string_values": "", + "default": "8", + "value": "8", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "away_priority": { "type": "integer", + "desc": "Resource priority on away", + "min": 0, + "max": 65535, + "string_values": "", + "default": "0", + "value": "0", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "password": { "type": "string", + "desc": "password for xmpp id on server", + "min": 0, + "max": 0, + "string_values": "", + "default": "", + "value": "", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "server": { "type": "string", + "desc": "connect server host or ip, eg. talk.google.com", + "min": 0, + "max": 0, + "string_values": "", + "default": "", + "value": "", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "ssl_ver": { "type": "string", + "desc": "Miniumum SSL version - empty for no SSL, else tlsv1.1|tlsv1.2|tlsv1.3", + "min": 0, + "max": 0, + "string_values": "", + "default": "", + "value": "", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "port": { "type": "integer", + "desc": "connect server port, eg. 5222 for not SSL", + "min": 0, + "max": 65535, + "string_values": "", + "default": "5222", + "value": "5222", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "autoconnect": { "type": "boolean", + "desc": "automatically connect to server when script is starting", + "min": 0, + "max": 0, + "string_values": "", + "default": "off", + "value": "off", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "autoreconnect": { "type": "boolean", + "desc": "automatically reconnect to server when disconnected", + "min": 0, + "max": 0, + "string_values": "", + "default": "off", + "value": "off", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "private": { "type": "boolean", + "desc": "display messages in separate chat buffers instead of a single server buffer", + "min": 0, + "max": 0, + "string_values": "", + "default": "on", + "value": "on", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "ping_interval": { "type": "integer", + "desc": "Number of seconds between server pings. 0 = disable", + "min": 0, + "max": 9999999, + "string_values": "", + "default": "0", + "value": "0", + "check_cb": "ping_interval_check_cb", + "change_cb": "", + "delete_cb": "", + }, + "ping_timeout": { "type": "integer", + "desc": "Number of seconds to allow ping to respond before timing out", + "min": 0, + "max": 9999999, + "string_values": "", + "default": "10", + "value": "10", + "check_cb": "ping_timeout_check_cb", + "change_cb": "", + "delete_cb": "", + }, + "CAfile": { "type": "string", + "desc": "CAfile - bundle of CA certificates in PEM", + "min": 0, + "max": 0, + "string_values": "", + "default": "/etc/ssl/certs/ca-certificates.crt", + "value": "", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "cert_file": { "type": "string", + "desc": "client certificate file for authentication, in PEM", + "min": 0, + "max": 0, + "string_values": "", + "default": "", + "value": "", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "key_file": { "type": "string", + "desc": "client private key file for authentication, in PEM", + "min": 0, + "max": 0, + "string_values": "", + "default": "", + "value": "", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + "ciphers": { "type": "string", + "desc": "ciphers for authentication, colon sep", + "min": 0, + "max": 0, + "string_values": "", + "default": "", + "value": "", + "check_cb": "", + "change_cb": "", + "delete_cb": "", + }, + } +jabber_config_file = None +jabber_config_section = {} +jabber_config_option = {} +jabber_jid_aliases = {} # { 'alias1': 'jid1', 'alias2': 'jid2', ... } + +class WeechatWrapper(object): + def __init__(self, wrapped_class): + self.wrapped_class = wrapped_class + + # Helper method used to encode/decode method calls. + def wrap_for_utf8(self, method): + def hooked(*args, **kwargs): + result = method(*encode_to_utf8(args), **encode_to_utf8(kwargs)) + # Prevent wrapped_class from becoming unwrapped + if result == self.wrapped_class: + return self + return decode_from_utf8(result) + + return hooked + + # Encode and decode everything sent to/received from weechat. We use the + # unicode type internally in wee-slack, but has to send utf8 to weechat. + def __getattr__(self, attr): + orig_attr = self.wrapped_class.__getattribute__(attr) + if callable(orig_attr): + return self.wrap_for_utf8(orig_attr) + else: + return decode_from_utf8(orig_attr) + + # Ensure all lines sent to weechat specifies a prefix. For lines after the + # first, we want to disable the prefix, which we do by specifying the same + # number of spaces, so it aligns correctly. + def prnt_date_tags(self, buffer, date, tags, message): + prefix, _, _ = message.partition("\t") + prefix = w.string_remove_color(encode_to_utf8(prefix), "") + prefix_spaces = " " * w.strlen_screen(prefix) + message = message.replace("\n", "\n{}\t".format(prefix_spaces)) + return self.wrap_for_utf8(self.wrapped_class.prnt_date_tags)( + buffer, date, tags, message + ) + +class ProxyWrapper(object): + def __init__(self): + self.proxy_name = w.config_string(w.config_get("weechat.network.proxy_curl")) + self.proxy_string = "" + self.proxy_type = "" + self.proxy_address = "" + self.proxy_port = "" + self.proxy_user = "" + self.proxy_password = "" + self.has_proxy = False + + if self.proxy_name: + self.proxy_string = "weechat.proxy.{}".format(self.proxy_name) + self.proxy_type = w.config_string( + w.config_get("{}.type".format(self.proxy_string)) + ) + if self.proxy_type == "http": + self.proxy_address = w.config_string( + w.config_get("{}.address".format(self.proxy_string)) + ) + self.proxy_port = w.config_integer( + w.config_get("{}.port".format(self.proxy_string)) + ) + self.proxy_user = w.config_string( + w.config_get("{}.username".format(self.proxy_string)) + ) + self.proxy_password = w.config_string( + w.config_get("{}.password".format(self.proxy_string)) + ) + self.has_proxy = True + else: + w_prnt( + "", + "\nWarning: weechat.network.proxy_curl is set to {} type (name: {}, conf string: {}). Only HTTP proxy is supported.\n\n".format( + self.proxy_type, self.proxy_name, self.proxy_string + ), + ) + + def curl(self): + if not self.has_proxy: + return "" + + if self.proxy_user and self.proxy_password: + user = "{}:{}@".format(self.proxy_user, self.proxy_password) + else: + user = "" + + if self.proxy_port: + port = ":{}".format(self.proxy_port) + else: + port = "" + + return "-x{}{}{}".format(user, self.proxy_address, port) + + +# =================================[ config ]================================= + +def jabber_config_init(): + """ Initialize config file: create sections and options in memory. """ + global jabber_config_file, jabber_config_section + jabber_config_file = w.config_new("jabber", "jabber_config_reload_cb", "") + if not jabber_config_file: + return + # look + jabber_config_section["look"] = w.config_new_section( + jabber_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", "") + if not jabber_config_section["look"]: + w.config_free(jabber_config_file) + return + jabber_config_option["debug"] = w.config_new_option( + jabber_config_file, jabber_config_section["look"], + "debug", "boolean", "display debug messages", "", 0, 0, + "off", "off", 0, "", "", "", "", "", "") + # color + jabber_config_section["color"] = w.config_new_section( + jabber_config_file, "color", 0, 0, "", "", "", "", "", "", "", "", "", "") + if not jabber_config_section["color"]: + w.config_free(jabber_config_file) + return + jabber_config_option["message_join"] = w.config_new_option( + jabber_config_file, jabber_config_section["color"], + "message_join", "color", "color for text in join messages", "", 0, 0, + "green", "green", 0, "", "", "", "", "", "") + jabber_config_option["message_quit"] = w.config_new_option( + jabber_config_file, jabber_config_section["color"], + "message_quit", "color", "color for text in quit messages", "", 0, 0, + "red", "red", 0, "", "", "", "", "", "") + # server + jabber_config_section["server"] = w.config_new_section( + jabber_config_file, "server", 0, 0, + "jabber_config_server_read_cb", "", "jabber_config_server_write_cb", "", + "", "", "", "", "", "") + if not jabber_config_section["server"]: + w.config_free(jabber_config_file) + return + jabber_config_section["jid_aliases"] = w.config_new_section( + jabber_config_file, "jid_aliases", 0, 0, + "jabber_config_jid_aliases_read_cb", "", + "jabber_config_jid_aliases_write_cb", "", + "", "", "", "", "", "") + if not jabber_config_section["jid_aliases"]: + w.config_free(jabber_config_file) + return + +def jabber_config_reload_cb(data, config_file): + """ Reload config file. """ + return w.config_reload(config_file) + +def jabber_config_server_read_cb(data, config_file, section, option_name, value): + """ Read server option in config file. """ + global jabber_servers + rc = w.WEECHAT_CONFIG_OPTION_SET_ERROR + items = option_name.split(".", 1) + if len(items) == 2: + server = jabber_search_server_by_name(items[0]) + if not server: + server = Server(items[0]) + jabber_servers.append(server) + if server: + rc = w.config_option_set(server.options[items[1]], value, 1) + return rc + +def jabber_config_server_write_cb(data, config_file, section_name): + """ Write server section in config file. """ + global jabber_servers + w.config_write_line(config_file, section_name, "") + for server in jabber_servers: + for name, option in sorted(server.options.items()): + w.config_write_option(config_file, option) + return w.WEECHAT_RC_OK + +def jabber_config_jid_aliases_read_cb(data, config_file, section, option_name, value): + """ Read jid_aliases option in config file. """ + global jabber_jid_aliases + jabber_jid_aliases[option_name] = value + option = w.config_new_option( + config_file, section, + option_name, "string", "jid alias", "", 0, 0, + "", value, 0, "", "", "", "", "", "") + if not option: + return w.WEECHAT_CONFIG_OPTION_SET_ERROR + return w.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED + +def jabber_config_jid_aliases_write_cb(data, config_file, section_name): + """ Write jid_aliases section in config file. """ + global jabber_jid_aliases + w.config_write_line(config_file, section_name, "") + for alias, jid in sorted(jabber_jid_aliases.items()): + w.config_write_line(config_file, alias, jid) + return w.WEECHAT_RC_OK + +def jabber_config_read(): + """ Read jabber config file (jabber.conf). """ + global jabber_config_file + return w.config_read(jabber_config_file) + +def jabber_config_write(): + """ Write jabber config file (jabber.conf). """ + global jabber_config_file + return w.config_write(jabber_config_file) + +def jabber_debug_enabled(): + """ Return True if debug is enabled. """ + global jabber_config_options + if w.config_boolean(jabber_config_option["debug"]): + return True + return False + +def jabber_config_color(color): + """ Return color code for a jabber color option. """ + global jabber_config_option + if color in jabber_config_option: + return w.color(w.config_color(jabber_config_option[color])) + return "" + +def ping_timeout_check_cb(server_name, option, value): + global jabber_config_file, jabber_config_section + ping_interval_option = w.config_search_option( + jabber_config_file, + jabber_config_section["server"], + "%s.ping_interval" % (server_name) + ) + ping_interval = w.config_integer(ping_interval_option) + if int(ping_interval) and int(value) >= int(ping_interval): + w_prnt("", "\njabber: unable to update 'ping_timeout' for server %s" % (server_name)) + w_prnt("", "jabber: to prevent multiple concurrent pings, ping_interval must be greater than ping_timeout") + return w.WEECHAT_CONFIG_OPTION_SET_ERROR + return w.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED + +def ping_interval_check_cb(server_name, option, value): + global jabber_config_file, jabber_config_section + ping_timeout_option = w.config_search_option( + jabber_config_file, + jabber_config_section["server"], + "%s.ping_timeout" % (server_name) + ) + ping_timeout = w.config_integer(ping_timeout_option) + if int(value) and int(ping_timeout) >= int(value): + w_prnt("", "\njabber: unable to update 'ping_interval' for server %s" % (server_name)) + w_prnt("", "jabber: to prevent multiple concurrent pings, ping_interval must be greater than ping_timeout") + return w.WEECHAT_CONFIG_OPTION_SET_ERROR + return w.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED + +import ssl +from xmpp.client import PlugIn +from xmpp.protocol import NodeProcessed +class HTTPPROXYsocket(xmpp.transports.TCPsocket): + """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class + redefines only connect method. Allows to use HTTP proxies like squid with + (optionally) simple authentication (using login and password). """ + def __init__(self, proxy, server, use_srv=True, buffer=None): + """ Caches proxy and target addresses. + 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) + and optional keys 'user' and 'password' to use for authentication. + 'server' argument is a tuple of host and port - just like TCPsocket uses. """ + xmpp.transports.TCPsocket.__init__(self, server, use_srv) + self.DBG_LINE = xmpp.transports.DBG_CONNECT_PROXY + self._proxy = proxy + self.buffer = buffer + + def connect(self, server=None): + """ Starts connection. Connects to proxy, supplies login and password to it + (if were specified while creating instance). Instructs proxy to make + connection to the target server. Returns non-empty sting on success. """ + if not xmpp.transports.TCPsocket.connect(self, + (self._proxy['host'], + self._proxy['port'])): + LOG_debug(f"Proxy not connect {(self._proxy['host'], self._proxy['port'])}",'start') + if self.buffer: + w_prnt(self.buffer, + "%sjabber: could not TCPsocket.connect" + % w.prefix("error")) + return + LOG_debug("Proxy server contacted, performing authentification",'start') + if not server: + server=self._server + connector = ['CONNECT %s:%s HTTP/1.0'%server, + 'Proxy-Connection: Keep-Alive', + 'Pragma: no-cache', + 'Host: %s:%s'%server, + 'User-Agent: HTTPPROXYsocket/v0.1'] + if 'user' in self._proxy and 'password' in self._proxy: + credentials = '%s:%s'%(self._proxy['user'],self._proxy['password']) + credentials = base64.encodestring(credentials).strip() + connector.append('Proxy-Authorization: Basic '+credentials) + connector.append('\r\n') + LOG_debug('Proxy sending connector','start') + # bytes? + self.send('\r\n'.join(connector)) + try: + reply = self.receive().replace(b'\r','') + reply = str(reply, 'UTF-8') + except IOError: + LOG_debug('Proxy suddenly disconnected','error') + self._owner.disconnected() + return + try: + proto, code, desc = reply.split('\n')[0].split(' ',2) + except Exception as e: + raise error(f'Invalid proxy reply {e}') + if code!='200': + LOG_debug('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error') + self._owner.disconnected() + return + while reply.find('\n\n') == -1: + try: + reply_more = self.receive().replace(b'\r','') + reply += str(reply_more, 'UTF-8') + except IOError: + LOG_debug('Proxy suddenly disconnected','error') + self._owner.disconnected() + return + LOG_debug("Authentification successfull. XMPP server contacted.",'ok') + return 'ok' + +class TLS(xmpp.transports.PlugIn): + """ TLS connection used to encrypts already estabilished tcp connection.""" + + def PlugIn(self, owner, now=True, assl_dict=None): + """ If the 'now' argument is true then starts using encryption immidiatedly. + If 'now' in false then starts encryption as soon as TLS feature is + declared by the server (if it were already declared - it is ok). + """ + if 'TLS' in owner.__dict__: + return # Already enabled. + xmpp.client.PlugIn.PlugIn(self, owner) + DBG_LINE = 'TLS' + if now: + if not assl_dict: + assl_dict=dict(keyfile=None, + certfile=None, + cert_reqs=ssl.CERT_NONE, + ssl_version=ssl.PROTOCOL_TLS, + ca_certs=None, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + ciphers=None) + return self._startSSL(**assl_dict) + if self._owner.Dispatcher.Stream.features: + try: + self.FeaturesHandler(self._owner.Dispatcher, + self._owner.Dispatcher.Stream.features) + except NodeProcessed: + pass + else: + self._owner.RegisterHandlerOnce('features', + self.FeaturesHandler, + xmlns=xmpp.protocol.NS_STREAMS) + self.starttls = None + + def _startSSL(self, + keyfile=None, + certfile=None, + cert_reqs=ssl.CERT_NONE, + ssl_version=ssl.PROTOCOL_TLS, + ca_certs=None, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + ciphers=None): + """ Immidiatedly switch socket to TLS mode. Used internally. + Here we should switch pending_data to hint mode.""" + if not ca_certs: + ca_certs = w.config_string(self.options['CAfile']) + tcpsock=self._owner.Connection + tcpsock._sslObj = ssl.wrap_socket(tcpsock._sock, + keyfile=None, + certfile=certfile, + cert_reqs=ssl.CERT_NONE, + ssl_version=ssl.PROTOCOL_TLS, + ca_certs=ca_certs, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + ciphers=None) + tcpsock._sslIssuer = tcpsock._sslObj.getpeercert().get('issuer') + tcpsock._sslServer = tcpsock._sslObj.getpeercert().get('server') + tcpsock._recv = tcpsock._sslObj.read + tcpsock._send = tcpsock._sslObj.write + + tcpsock._seen_data = 1 + self._tcpsock=tcpsock + tcpsock.pending_data=self.pending_data + tcpsock._sslObj.setblocking(False) + + self.starttls='success' + +from xmpp import transports, dispatcher +class CommonClient(xmpp.client.CommonClient): + + def __init__(self, server, + port=5222, + debug=[], # 'always', 'nodebuilder' + buffer=None, + assl_dict=None): + xmpp.client.CommonClient.__init__(self, server, port, debug=debug) + self.buffer = buffer + self.assl_dict = assl_dict + + def connect(self,server_tuple=None, proxy=None, ssl=None,use_srv=False,transport=None): + """ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream. + Returns None or 'tcp' or 'tls', depending on the result.""" + if not server_tuple: + server_tuple=(self.Server, self.Port) + self.assl_dict = None + + # bulletproofing + assert type(server_tuple) == tuple, server_tuple + assert len(server_tuple) >= 2, server_tuple + assert type(server_tuple[1]) in [int, str], server_tuple + assert type(server_tuple[0]) in [bytes, str], server_tuple + assert server_tuple[0] and server_tuple[1], server_tuple + + if transport: + pass + elif proxy: + transport = HTTPPROXYsocket(proxy, server_tuple, use_srv, ) + else: + transport = xmpp.transports.TCPsocket(server_tuple, use_srv) + if self.buffer: + w_prnt(self.buffer, + f"{w.prefix('network')}jabber: proxy: transport={transport}") + connected = transport.PlugIn(self) + if not connected: + serr = f"Failed to transport.PlugIn(self)" + LOG_debug(serr,'error') + transport.PlugOut() + return serr + self.connected = 'tcp' + + self._server_tuple = server_tuple + self._aProxy = proxy + if (ssl is None and self.Connection.getPort() in (5223, 443)): + ssl = True + if ssl: + try: # FIXME. This should be done in transports.py + TLS().PlugIn(self, now=True, assl_dict=self.assl_dict) + self.connected = 'ssl' + except socket.sslerror as e: + serr = f"Failed to transports.TLS().PlugIn(self, now=1)" + LOG_debug(serr,'error') + return serr + + dispatcher.Dispatcher().PlugIn(self) + while self.Dispatcher.Stream._document_attrs is None: + if not self.Process(1): + serr = f"Failed to dispatcher.Dispatcher().PlugIn(self)" + LOG_debug(serr,'error') + return serr + + if 'version' in self.Dispatcher.Stream._document_attrs and self.Dispatcher.Stream._document_attrs['version']=='1.0': + while not self.Dispatcher.Stream.features and self.Process(1): + # If we get version 1.0 stream the features tag MUST BE presented + pass + + return self.connected + +class Client(CommonClient): + """ Example client class, based on CommonClient. """ + def __init__(self, server, + port, + debug=[], # 'always', 'nodebuilder'] + buffer=None, + assl_dict=None): + self.buffer = buffer + if not assl_dict: + assl_dict=dict(keyfile=None, + certfile=None, + cert_reqs=ssl.CERT_NONE, + ssl_version=ssl.PROTOCOL_TLS, + ca_certs=None, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + ciphers=None) + self.assl_dict = assl_dict + # no ssl= + CommonClient.__init__(self, + server, + port, + buffer=self.buffer, + debug=debug) + + def connect(self, server_tuple=None, proxy=None, secure=None, use_srv=False, transport=None): + """Connect to XMPP server_tuple. If you want to specify different ip/port + to connect to you can pass it as tuple as first parameter. + If there is HTTP proxy between you and server + specify it's address and credentials (if needed) in the second argument. + + If you want ssl/tls support to be discovered and enable + automatically - leave third argument as None. (ssl will be + autodetected only if port is 5223 or 443) + + If you want to force SSL start (i.e. if port 5223 or 443 is + remapped to some non-standard port) then set it to 1. + + If you want to disable tls/ssl support completely, set it to 0. + + Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'}) + Returns '' or 'tcp' or 'tls', depending on the result. + + """ + assert type(proxy) == dict + try: + cc = CommonClient(self) + self.connected = cc.connect(server_tuple, + proxy=proxy, + ssl=secure, + use_srv=use_srv, + transport=transport) + except Exception as e: + if self.buffer: + oerror = str(e) + ' ' + traceback.format_exc() + w_prnt(self.buffer, f"Error CCconnect {oerror} proxy={proxy}") + return '' + self.connected = 'tcp' + + if self.connected not in ['ssl', 'tcp']: + if self.buffer: + w_prnt(self.buffer, f"Error NOT Connected to {server_tuple} {self.connected}") + return '' + + if secure is not None and not secure: + # 0 or False + if self.buffer: + w_prnt(self.buffer, f"Connected {self.connected} to {server_tuple} proxy={proxy}") + return self.connected + + TLS().PlugIn(self, assl_dict=self.assl_dict) + + if 'version' not in self.Dispatcher.Stream._document_attrs or \ + not self.Dispatcher.Stream._document_attrs['version']=='1.0': + return self.connected + + while not self.Dispatcher.Stream.features and self.Process(1): + pass # If we get version 1.0 stream the features tag MUST BE presented + if not self.Dispatcher.Stream.features.getTag('starttls'): + return self.connected # TLS not supported by server + while not self.TLS.starttls and self.Process(1): + pass + if not hasattr(self, 'TLS') or self.TLS.starttls != 'success': + self.event('tls_failed') + return self.connected + self.connected = 'tls' + return self.connected + +class Server: + """ Class to manage a server: buffer, connection, send/recv data. """ + + def __init__(self, name, **kwargs): + """ Init server """ + global jabber_config_file, jabber_config_section, jabber_server_options + self.name = name + # create options (user can set them with /set) + self.options = {} + # if the value is provided, use it, otherwise use the default + values = {} + for option_name, props in jabber_server_options.items(): + values[option_name] = props["default"] + values['name'] = name + values.update(**kwargs) + for option_name, props in jabber_server_options.items(): + self.options[option_name] = w.config_new_option( + jabber_config_file, + jabber_config_section["server"], + self.name + "." + option_name, props["type"], + props["desc"], + props["string_values"], + props["min"], + props["max"], + props["default"], + values[option_name], 0, + props["check_cb"], self.name, props["change_cb"], "", + props["delete_cb"], "") + # internal data + self.jid = None + self.client = None + self.sock = None + self.hook_fd = None + self.buffer = "" + self.chats = [] + self.roster = None + self.buddies = [] + self.buddy = None + self.ping_timer = None # weechat.hook_timer for sending pings + self.ping_timeout_timer = None # weechat.hook_timer for monitoring ping timeout + self.ping_up = False # Connection status as per pings. + self.presence = xmpp.protocol.Presence() + + def option_string(self, option_name): + """ Return a server option, as string. """ + return w.config_string(self.options[option_name]) + + def option_boolean(self, option_name): + """ Return a server option, as boolean. """ + return w.config_boolean(self.options[option_name]) + + def option_integer(self, option_name): + """ Return a server option, as string. """ + return w.config_integer(self.options[option_name]) + + def make_proxy(self): + proxy = {} + pair = os.environ.get('https_proxy', '') + pair = pair.replace('https://', '') + pair = pair.replace('http://', '') + if ':' in pair: + phost, pport = pair.split(':', 1) + # '127.0.0.1' 9128 + proxy = {'host': phost, 'port': int(pport)} + return proxy + + def make_buffer(self): + bufname = "%s.server.%s" % (SCRIPT_NAME, self.name) + self.buffer = w.buffer_search("python", bufname) + if not self.buffer: + self.buffer = w.buffer_new(bufname, + "jabber_buffer_input_cb", "", + "jabber_buffer_close_cb", "") + if self.buffer: + w.buffer_set(self.buffer, "short_name", self.name) + w.buffer_set(self.buffer, "localvar_set_type", "server") + w.buffer_set(self.buffer, "localvar_set_server", self.name) + w.buffer_set(self.buffer, "nicklist", "1") + w.buffer_set(self.buffer, "nicklist_display_groups", "1") + w.buffer_set(self.buffer, "display", "auto") + + def connect(self): + """ Connect to Jabber server. """ + try: + self.connect_() + except Exception as e: + oerror = str(e) + ' ' + traceback.format_exc() + w_prnt(self.buffer, f"%sError during connect {oerror}" + % w.prefix("error")) + + def connect_(self): + """ Connect to Jabber server. """ + if not self.buffer: + self.make_buffer() + self.disconnect() + + if not eval_expression(self.option_string("jid")): + w_prnt(self.buffer, "%sjabber: JID must contain at least domain name" + % w.prefix("error")) + self.ping_up = False + self.client = None + return self.is_connected() + + # server: Server object instance + self.buddy = Buddy(jid=eval_expression(self.option_string("jid")), server=self) + + server = self.option_string("server") + port = self.option_integer("port") + + secure = False + if port == 5222: + secure = False + elif port == 5223: + secure = True + elif ssl_ver != '': + secure = True + + if secure is False: + assl_dict = dict() + else: + ca_certs = w.config_string(self.options['CAfile']) + certfile = w.config_string(self.options['cert_file']) + keyfile = w.config_string(self.options['key_file']) + ssl_ver = w.config_string(self.options['ssl_ver']) + assert ssl_ver in ['', 'tlsv1.1', 'tlsv1.2', 'tlsv1.3'] + # CERT_NONE: 175f. (certificates ignored), + # CERT_OPTIONAL: 1760. (not required, but validated if provided), + # CERT_REQUIRED: 1761. (required and validated). + if ssl_ver == 'tlsv1': + cert_reqs = ssl.CERT_OPTIONAL + elif keyfile or certfile: + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_REQUIRED + ciphers = w.config_string(self.options['ciphers']) + # http://www.openssl.org/docs/apps/ciphers.html#CIPHER_LIST_FORMAT + assl_dict = dict(keyfile=keyfile, + certfile=certfile, + cert_reqs=cert_reqs, + ssl_version=ssl.PROTOCOL_TLS, + ca_certs=ca_certs, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + ciphers=ciphers) + self.client = Client(self.buddy.domain, + port, + debug=[], + buffer=self.buffer, + assl_dict=assl_dict, + ) + conn = None + server_tuple = None + if not server: + # override + # pulled up from self.client.connect + server = self.buddy.domain + if port: + server_tuple = (server, int(port)) + else: + # override + # pulled up from self.client.connect + server_tuple = (server, 5222) + + proxy = {} + if os.environ.get('https_proxy', ''): + proxy = self.make_proxy() + + # self.client.connect() may produce a "socket.ssl() is deprecated" + # warning. Since the code producing the warning is outside this script, + # catch it and ignore. + original_filters = warnings.filters[:] + warnings.filterwarnings("ignore", category=DeprecationWarning) + oerror = None + conn = None + use_srv = False + if False and proxy: + transport = HTTPPROXYsocket(proxy, + server_tuple, + use_srv=use_srv, + buffer=self.buffer) + w_prnt(self.buffer, + f"{w.prefix('network')}jabber: proxy: transport={transport}" + ) + else: + transport = None + try: + conn = self.client.connect(server_tuple, + proxy=proxy, + secure=secure, + use_srv=False, + transport=transport, + ) + except BaseException as e: + oerror = str(e) + ' ' + traceback.format_exc() + finally: + warnings.filters = original_filters + + if not conn: + w_prnt(self.buffer, + f"{w.prefix('error')}jabber: could not connect: conn={conn} oerror={oerror}" + ) + self.ping_up = False + self.client = None + else: + w_prnt(self.buffer, "jabber: connection ok with %s" % conn) + #? + self.ping_up = True + res = self.buddy.resource + if not res: + res = "WeeChat" + + w_prnt(self.buffer, f"jabber: auth as {self.buddy.username} {dir(self.client)}") + auth = self.client.auth(self.buddy.username, + eval_expression(self.option_string("password")), + res) + + if auth: + w_prnt(self.buffer, f"{w.prefix('network')}authentication ok {auth}") + + self.roster = self.client.getRoster() + self.client.RegisterHandler("presence", self.presence_handler) + self.client.RegisterHandler("iq", self.iq_handler) + self.client.RegisterHandler("message", self.message_handler) + self.client.sendInitPresence(requestRoster=1) + self.sock = self.client.Connection._sock.fileno() + self.hook_fd = w.hook_fd(self.sock, 1, 0, 0, "jabber_fd_cb", "") + w.buffer_set(self.buffer, "highlight_words", self.buddy.username) + w.buffer_set(self.buffer, "localvar_set_nick", self.buddy.username); + hook_away = w.hook_command_run("/away -all*", "jabber_away_command_run_cb", "") + + + # setting initial presence + priority = w.config_integer(self.options['priority']) + self.set_presence(show="",priority=priority) + + + self.ping_up = True + else: + w_prnt(self.buffer, "%sjabber: could not authenticate" + % w.prefix("error")) + self.ping_up = False + self.client = None + return self.is_connected() + + def is_connected(self): + """Return connect status""" + if not self.client or not self.client.isConnected(): + return False + else: + return True + + def add_chat(self, buddy): + """Create a chat buffer for a buddy""" + chat = Chat(self, buddy, switch_to_buffer=False) + self.chats.append(chat) + return chat + + def add_buddy(self, jid): + """ Add a new buddy """ + self.client.Roster.Authorize(jid) + self.client.Roster.Subscribe(jid) + + def del_buddy(self, jid): + """ Remove a buddy and/or deny authorization request """ + self.client.Roster.Unauthorize(jid) + self.client.Roster.Unsubscribe(jid) + + def print_debug_server(self, message): + """ Print debug message on server buffer. """ + if jabber_debug_enabled(): + w_prnt(self.buffer, "%sjabber: %s" % (w.prefix("network"), message)) + + def print_debug_handler(self, handler_name, node): + """ Print debug message for a handler on server buffer. """ + self.print_debug_server("%s_handler, xml message:\n%s" + % (handler_name, + node.__str__(fancy=True).encode("utf-8"))) + + def print_error(self, message): + """ Print error message on server buffer. """ + if jabber_debug_enabled(): + w_prnt(self.buffer, "%sjabber: %s" % (w.prefix("error"), message)) + + def presence_handler(self, conn, node): + self.print_debug_handler("presence", node) + buddy = self.search_buddy_list(node.getFrom().getStripped().encode("utf-8"), by='jid') + if not buddy: + buddy = self.add_buddy(jid=node.getFrom()) + action='update' + node_type = node.getType() + if node_type in ["error", "unavailable"]: + action='remove' + if action == 'update': + away = node.getShow() in ["away", "xa"] + status = '' + if node.getStatus(): + status = node.getStatus().encode("utf-8") + if self.roster: + name = self.roster.getName(buddy.bare_jid) + if name: + buddy.set_name(name.encode("utf-8")) + buddy.set_status(status=status, away=away) + self.update_nicklist(buddy=buddy, action=action) + return + + def iq_handler(self, conn, node): + """ Receive iq message. """ + self.print_debug_handler("iq", node) + #w_prnt(self.buffer, "jabber: iq handler") + if node.getFrom() == self.buddy.domain: + # type='result' => pong from server + # type='error' => error message from server + # The ping_up is set True on an error message to handle cases where + # the ping feature is not implemented on a server. It's a bit of a + # hack, but if we can receive an error from the server, we assume + # the connection to the server is up. + if node.getType() in ['result', 'error']: + self.delete_ping_timeout_timer() # Disable the timeout feature + self.ping_up = True + if not self.client.isConnected() and w.config_boolean(self.options['autoreconnect']): + self.connect() + + def message_handler(self, conn, node): + """ Receive message. """ + self.print_debug_handler("message", node) + node_type = node.getType() + if node_type not in ["message", "chat", None]: + self.print_error("unknown message type: '%s'" % node_type) + return + jid = node.getFrom() + body = node.getBody() + if not jid or not body: + return + buddy = self.search_buddy_list(self.stringify_jid(jid), by='jid') + if not buddy: + buddy = self.add_buddy(jid=jid) + # If a chat buffer exists for the buddy, receive the message with that + # buffer even if private is off. The buffer may have been created with + # /jchat. + recv_object = self + if not buddy.chat and w.config_boolean(self.options['private']): + self.add_chat(buddy) + if buddy.chat: + recv_object = buddy.chat + recv_object.recv_message(buddy, body.encode("utf-8")) + + def recv(self): + """ Receive something from Jabber server. """ + if not self.client: + return + try: + self.client.Process(1) + except xmpp.protocol.StreamError as e: + w_prnt('', '%s: Error from server: %s' %(SCRIPT_NAME, e)) + self.disconnect() + if w.config_boolean(self.options['autoreconnect']): + autoreconnect_delay = 30 + w.command('', '/wait %s /%s connect %s' % + (autoreconnect_delay, SCRIPT_COMMAND, self.name)) + + def recv_message(self, buddy, message): + """ Receive a message from buddy. """ + w_prnt_date_tags(self.buffer, 0, + "notify_private,nick_%s,prefix_nick_%s,log1" % + (buddy.alias, + w.config_string(w.config_get("weechat.color.chat_nick_other"))), + "%s%s\t%s" % (w.color("chat_nick_other"), + buddy.alias, + message)) + + def print_status(self, nickname, status): + """ Print a status in server window and in chat. """ + w_prnt_date_tags(self.buffer, 0, "no_highlight", "%s%s has status %s" % + (w.prefix("action"), + nickname, + status)) + for chat in self.chats: + if nickname in chat.buddy.alias: + chat.print_status(status) + break + + def send_message(self, buddy, message): + """ Send a message to buddy. + + The buddy argument can be either a jid string, + eg username@domain.tld/resource or a Buddy object instance. + """ + recipient = buddy + if isinstance(buddy, Buddy): + recipient = buddy.jid + if not self.ping_up: + w_prnt(self.buffer, "%sjabber: unable to send message, connection is down" + % w.prefix("error")) + return + if self.client: + msg = xmpp.protocol.Message(to=recipient, body=message, typ='chat') + self.client.send(msg) + + def send_message_from_input(self, input=''): + """ Send a message from input text on server buffer. """ + # Input must be of format "name: message" where name is a jid, bare_jid + # or alias. The colon can be replaced with a comma as well. + # Split input into name and message. + if not re.compile(r'.+[:,].+').match(input): + w_prnt(self.buffer, "%sjabber: %s" % (w.prefix("network"), + "Invalid send format. Use jid: message" + )) + return + name, message = re.split('[:,]', input, maxsplit=1) + buddy = self.search_buddy_list(name, by='alias') + if not buddy: + w_prnt(self.buffer, + "%sjabber: Invalid jid: %s" % (w.prefix("network"), + name)) + return + # Send activity indicates user is no longer away, set it so + if self.buddy and self.buddy.away: + self.set_away('') + self.send_message(buddy=buddy, message=message) + try: + sender = self.buddy.alias + except: + sender = self.jid + w_prnt_date_tags(self.buffer, 0, + "notify_none,no_highlight,nick_%s,prefix_nick_%s,log1" % + (sender, + w.config_string(w.config_get("weechat.color.chat_nick_self"))), + "%s%s\t%s" % (w.color("chat_nick_self"), + sender, + message.strip())) + + def set_away(self, message): + """ Set/unset away on server. + + If a message is provided, status is set to 'away'. + If no message, then status is set to 'online'. + """ + if message: + show = "xa" + status = message + priority = w.config_integer(self.options['away_priority']) + self.buddy.set_status(away=True, status=message) + else: + show = "" + status = None + priority = w.config_integer(self.options['priority']) + self.buddy.set_status(away=False) + self.set_presence(show, status, priority) + + def set_presence(self, show=None, status=None, priority=None): + if not show == None: self.presence.setShow(show) + if not status == None: self.presence.setStatus(status) + if not priority == None: self.presence.setPriority(priority) + self.client.send(self.presence) + + def add_buddy(self, jid=None): + buddy = Buddy(jid=jid, server=self) + buddy.resource = buddy.resource.encode("utf-8") + self.buddies.append(buddy) + return buddy + + def display_buddies(self): + """ Display buddies. """ + w_prnt(self.buffer, "") + w_prnt(self.buffer, "Buddies:") + + len_max = { 'alias': 5, 'jid': 5 } + lines = [] + for buddy in sorted(self.buddies, key=lambda x: x.jid.getStripped().encode('utf-8')): + alias = '' + if buddy.alias != buddy.bare_jid: + alias = buddy.alias + buddy_jid_string = buddy.jid.getStripped().encode('utf-8') + lines.append( { + 'jid': buddy_jid_string, + 'alias': alias, + 'status': buddy.away_string(), + }) + if len(alias) > len_max['alias']: + len_max['alias'] = len(alias) + if len(buddy_jid_string) > len_max['jid']: + len_max['jid'] = len(buddy_jid_string) + prnt_format = " %s%-" + str(len_max['jid']) + "s %-" + str(len_max['alias']) + "s %s" + w_prnt(self.buffer, prnt_format % ('', 'JID', 'Alias', 'Status')) + for line in lines: + w_prnt(self.buffer, prnt_format % (w.color("chat_nick"), + line['jid'], + line['alias'], + line['status'], + )) + + def stringify_jid(self, jid, wresource=1): + """ Serialise JID into string. + + Args: + jid: xmpp.protocol.JID, JID instance to serialize + + Notes: + Method is based on original JID.__str__ but with hack to allow + non-ascii in resource names. + """ + if jid.node: + jid_str = jid.node + '@' + jid.domain + else: + jid_str = jid.domain + if wresource and jid.resource: + # concatenate jid with resource delimiter first and encode them + # into utf-8, else it will raise UnicodeException becaouse of + # slash character:(( + return (jid_str + '/').encode("utf-8") + jid.resource.encode("utf-8") + return jid_str.encode("utf-8") + + def search_buddy_list(self, name, by='jid'): + """ Search for a buddy by name. + + Args: + name: string, the buddy name to search, eg the jid or alias + by: string, either 'alias' or 'jid', determines which Buddy + property to match on, default 'jid' + + Notes: + If the 'by' parameter is set to 'jid', the search matches on all + Buddy object jid properties, followed by all bare_jid properties. + Once a match is found it is returned. + + If the 'by' parameter is set to 'alias', the search matches on all + Buddy object alias properties. + + Generally, set the 'by' parameter to 'jid' when the jid is provided + from a server, for example from a received message. Set 'by' to + 'alias' when the jid is provided by the user. + """ + if by == 'jid': + for buddy in self.buddies: + if self.stringify_jid(buddy.jid) == name: + return buddy + for buddy in self.buddies: + if buddy.bare_jid == name: + return buddy + else: + for buddy in self.buddies: + if buddy.alias == name: + return buddy + return None + + def update_nicklist(self, buddy=None, action=None): + """Update buddy in nicklist + Args: + buddy: Buddy object instance + action: string, one of 'update' or 'remove' + """ + if not buddy: + return + if not action in ['remove', 'update']: + return + ptr_nick_gui = w.nicklist_search_nick(self.buffer, "", buddy.alias) + w.nicklist_remove_nick(self.buffer, ptr_nick_gui) + msg = '' + prefix = '' + color = '' + away = '' + if action == 'update': + nick_color = "bar_fg" + if buddy.away: + nick_color = "weechat.color.nicklist_away" + w.nicklist_add_nick(self.buffer, "", buddy.alias, + nick_color, "", "", 1) + if not ptr_nick_gui: + msg = 'joined' + prefix = 'join' + color = 'message_join' + away = buddy.away_string() + if action == 'remove': + msg = 'quit' + prefix = 'quit' + color = 'message_quit' + if msg: + w_prnt(self.buffer, "%s%s%s%s has %s %s" + % (w.prefix(prefix), + w.color("chat_nick"), + buddy.alias, + jabber_config_color(color), + msg, + away)) + return + + def add_ping_timer(self): + if self.ping_timer: + self.delete_ping_timer() + if not self.option_integer('ping_interval'): + return + self.ping_timer = w.hook_timer( self.option_integer('ping_interval') * 1000, + 0, 0, "jabber_ping_timer", self.name) + return + + def delete_ping_timer(self): + if self.ping_timer: + w.unhook(self.ping_timer) + self.ping_time = None + return + + def add_ping_timeout_timer(self): + if self.ping_timeout_timer: + self.delete_ping_timeout_timer() + if not self.option_integer('ping_timeout'): + return + self.ping_timeout_timer = w.hook_timer( + self.option_integer('ping_timeout') * 1000, 0, 1, + "jabber_ping_timeout_timer", self.name) + return + + def delete_ping_timeout_timer(self): + if self.ping_timeout_timer: + w.unhook(self.ping_timeout_timer) + self.ping_timeout_timer = None + return + + def ping(self): + if not self.is_connected(): + if not self.connect(): + return + iq = xmpp.protocol.Iq(to=self.buddy.domain, typ='get') + iq.addChild( name= "ping", namespace = "urn:xmpp:ping" ) + id = self.client.send(iq) + self.print_debug_handler("ping", iq) + self.add_ping_timeout_timer() + return + + def ping_time_out(self): + self.delete_ping_timeout_timer() + self.ping_up = False + # A ping timeout indicates a server connection problem. Disconnect + # completely. + try: + self.client.disconnected() + except IOError: + # An IOError is raised by the default DisconnectHandler + pass + self.disconnect() + return + + def disconnect(self): + """ Disconnect from Jabber server. """ + if self.hook_fd != None: + w.unhook(self.hook_fd) + self.hook_fd = None + if self.client != None: + #if self.client.isConnected(): + # self.client.disconnect() + self.client = None + self.jid = None + self.sock = None + self.buddy = None + w.nicklist_remove_all(self.buffer) + + def close_buffer(self): + """ Close server buffer. """ + if self.buffer != "": + w.buffer_close(self.buffer) + self.buffer = "" + + def delete(self, deleteOptions=False): + """ Delete server. """ + for chat in self.chats: + chat.delete() + self.delete_ping_timer() + self.delete_ping_timeout_timer() + self.disconnect() + self.close_buffer() + if deleteOptions: + for name, option in self.options.items(): + w.config_option_free(option) + +def eval_expression(option_name): + """ Return a evaluated expression """ + version = int(w.info_get("version_number", "") or 0) + if int(version) >= 0x00040200: + return w.string_eval_expression(option_name,{},{},{}) + else: + return option_name + +def jabber_search_server_by_name(name): + """ Search a server by name. """ + global jabber_servers + for server in jabber_servers: + if server.name == name: + return server + return None + +def jabber_search_context(buffer): + """ Search a server / chat for a buffer. """ + global jabber_servers + context = { "server": None, "chat": None } + for server in jabber_servers: + if server.buffer == buffer: + context["server"] = server + return context + for chat in server.chats: + if chat.buffer == buffer: + context["server"] = server + context["chat"] = chat + return context + return context + +def jabber_search_context_by_name(server_name): + """Search for buffer given name of server. """ + + bufname = "%s.server.%s" % (SCRIPT_NAME, server_name) + return jabber_search_context(w.buffer_search("python", bufname)) + + +# =================================[ chats ]================================== + +class Chat: + """ Class to manage private chat with buddy or MUC. """ + + def __init__(self, server, buddy, switch_to_buffer): + """ Init chat """ + self.server = server + self.buddy = buddy + buddy.chat = self + bufname = "%s.%s.%s" % (SCRIPT_NAME, server.name, self.buddy.alias) + self.buffer = w.buffer_search("python", bufname) + if not self.buffer: + self.buffer = w.buffer_new(bufname, + "jabber_buffer_input_cb", "", + "jabber_buffer_close_cb", "") + self.buffer_title = self.buddy.alias + if self.buffer: + w.buffer_set(self.buffer, "title", self.buffer_title) + w.buffer_set(self.buffer, "short_name", self.buddy.alias) + w.buffer_set(self.buffer, "localvar_set_type", "private") + w.buffer_set(self.buffer, "localvar_set_server", server.name) + w.buffer_set(self.buffer, "localvar_set_channel", self.buddy.alias) + w.hook_signal_send("logger_backlog", + w.WEECHAT_HOOK_SIGNAL_POINTER, self.buffer) + if switch_to_buffer: + w.buffer_set(self.buffer, "display", "auto") + + def recv_message(self, buddy, message): + """ Receive a message from buddy. """ + if buddy.alias != self.buffer_title: + self.buffer_title = buddy.alias + w.buffer_set(self.buffer, "title", "%s" % self.buffer_title) + w_prnt_date_tags(self.buffer, 0, + "notify_private,nick_%s,prefix_nick_%s,log1" % + (buddy.alias, + w.config_string(w.config_get("weechat.color.chat_nick_other"))), + "%s%s\t%s" % (w.color("chat_nick_other"), + buddy.alias, + message)) + + def send_message(self, message): + """ Send message to buddy. """ + if not self.server.ping_up: + w_prnt(self.buffer, "%sxmpp: unable to send message, connection is down" + % w.prefix("error")) + return + self.server.send_message(self.buddy, message) + w_prnt_date_tags(self.buffer, 0, + "notify_none,no_highlight,nick_%s,prefix_nick_%s,log1" % + (self.server.buddy.alias, + w.config_string(w.config_get("weechat.color.chat_nick_self"))), + "%s%s\t%s" % (w.color("chat_nick_self"), + self.server.buddy.alias, + message)) + def print_status(self, status): + """ Print a status message in chat. """ + w_prnt(self.buffer, "%s%s has status %s" % + (w.prefix("action"), + self.buddy.alias, + status)) + + def close_buffer(self): + """ Close chat buffer. """ + if self.buffer != "": + w.buffer_close(self.buffer) + self.buffer = "" + + def delete(self): + """ Delete chat. """ + self.close_buffer() + +# =================================[ buddies ]================================== + +class Buddy: + """ Class to manage buddies. """ + def __init__(self, jid=None, chat=None, server=None ): + """ Init buddy + + Args: + jid: xmpp.protocol.JID object instance or string + chat: Chat object instance + server: Server object instance + + The jid argument can be provided either as a xmpp.protocol.JID object + instance or as a string, eg "username@domain.tld/resource". If a string + is provided, it is converted and stored as a xmpp.protocol.JID object + instance. + """ + + # The jid argument of xmpp.protocol.JID can be either a string or a + # xmpp.protocol.JID object instance itself. + self.jid = xmpp.protocol.JID(jid=jid) + self.chat = chat + self.server = server + self.bare_jid = '' + self.username = '' + self.name = '' + self.domain = '' + self.resource = '' + self.alias = '' + self.away = True + self.status = '' + + self.parse_jid() + self.set_alias() + return + + def away_string(self): + """ Return a string with away and status, with color codes. """ + if not self: + return '' + if not self.away: + return '' + str_colon = ": " + if not self.status: + str_colon = "" + return "%s(%saway%s%s%s)" % (w.color("chat_delimiters"), + w.color("chat"), + str_colon, + self.status.replace("\n", " "), + w.color("chat_delimiters")) + + def parse_jid(self): + """Parse the jid property. + + The table shows how the jid is parsed and which properties are updated. + + Property Value + jid myuser@mydomain.tld/myresource + + bare_jid myuser@mydomain.tld + username myuser + domain mydomain.tld + resource myresource + """ + if not self.jid: + return + self.bare_jid = self.jid.getStripped().encode("utf-8") + self.username = self.jid.getNode() + self.domain = self.jid.getDomain() + self.resource = self.jid.getResource() + return + + def set_alias(self): + """Set the buddy alias. + + If an alias is defined in jabber_jid_aliases, it is used. Otherwise the + alias is set to self.bare_jid or self.name if it exists. + """ + self.alias = self.bare_jid + if not self.bare_jid: + self.alias = '' + if self.name: + self.alias = self.name + global jabber_jid_aliases + for alias, jid in jabber_jid_aliases.items(): + if jid == self.bare_jid: + self.alias = alias + break + return + + def set_name(self, name=''): + self.name = name + self.set_alias() + return + + def set_status(self, away=True, status=''): + """Set the buddy status. + + Two properties define the buddy status. + away - boolean, indicates whether the buddy is away or not. + status - string, a message indicating the away status, eg 'in a meeting' + Comparable to xmpp presence element. + """ + if not away and not status: + status = 'online' + # If the status has changed print a message on the server buffer + if self.away != away or self.status != status: + self.server.print_status(self.alias, status) + self.away = away + self.status = status + return + +# ================================[ commands ]================================ + +def jabber_hook_commands_and_completions(): + """ Hook commands and completions. """ + w.hook_command(SCRIPT_COMMAND, "Manage Jabber servers", + "list || add [[:]]" + " || connect|disconnect|del [] || alias [add|del ]" + " || away [] || buddies || priority []" + " || status [] || presence [online|chat|away|xa|dnd]" + " || debug || set []", + " list: list servers and chats\n" + " add: add a server\n" + " connect: connect to server using password\n" + "disconnect: disconnect from server\n" + " del: delete server\n" + " alias: manage jid aliases\n" + " away: set away with a message (if no message, away is unset)\n" + " priority: set priority\n" + " status: set status message\n" + " presence: set presence status\n" + " buddies: display buddies on server\n" + " debug: toggle jabber debug on/off (for all servers)\n" + "\n" + "Without argument, this command lists servers and chats.\n" + "\n" + "Examples:\n" + " Add a server: /jabber add myserver user@server.tld password\n" + " Add gtalk server: /jabber add myserver user@gmail.com password talk.google.com:5223\n" + " Connect to server: /jabber connect myserver\n" + " Disconnect: /jabber disconnect myserver\n" + " Delete server: /jabber del myserver\n" + "\n" + "Aliases:\n" + " List aliases: /jabber alias \n" + " Add an alias: /jabber alias add alias_name jid\n" + " Delete an alias: /jabber alias del alias_name\n" + "\n" + "Other jabber commands:\n" + " Chat with a buddy (pv buffer): /jchat\n" + " Add buddy to roster: /invite\n" + " Remove buddy from roster: /kick\n" + " Send message to buddy: /jmsg", + "list %(jabber_servers)" + " || add %(jabber_servers)" + " || connect %(jabber_servers)" + " || disconnect %(jabber_servers)" + " || del %(jabber_servers)" + " || alias add|del %(jabber_jid_aliases)" + " || away" + " || priority" + " || status" + " || presence online|chat|away|xa|dnd" + " || buddies" + " || debug", + "jabber_cmd_jabber", "") + w.hook_command("jchat", "Chat with a Jabber buddy", + "", + "buddy: buddy id", + "", + "jabber_cmd_jchat", "") + w.hook_command("jmsg", "Send a messge to a buddy", + "[-server ] ", + "server: name of jabber server buddy is on\n" + " buddy: buddy id\n" + " text: text to send", + "", + "jabber_cmd_jmsg", "") + w.hook_command("invite", "Add a buddy to your roster", + "", + "buddy: buddy id", + "", + "jabber_cmd_invite", "") + w.hook_command("kick", "Remove a buddy from your roster, or deny auth", + "", + "buddy: buddy id", + "", + "jabber_cmd_kick", "") + w.hook_completion("jabber_servers", "list of jabber servers", + "jabber_completion_servers", "") + w.hook_completion("jabber_jid_aliases", "list of jabber jid aliases", + "jabber_completion_jid_aliases", "") + +def jabber_list_servers_chats(name): + """ List servers and chats. """ + global jabber_servers + w_prnt("", "") + if len(jabber_servers) > 0: + w_prnt("", "jabber servers:") + for server in jabber_servers: + if name == "" or server.name.find(name) >= 0: + conn_server = '' + if server.option_string("server"): + conn_server = ':'.join( + (server.option_string("server"), + server.option_string("port"))) + connected = "" + if server.sock and server.sock >= 0: + connected = "(connected)" + else: + connected = "(not conn)" + + w_prnt("", " %s - %s %s %s" % (server.name, + eval_expression(server.option_string("jid")), conn_server, connected)) + for chat in server.chats: + w_prnt("", " chat with %s" % (chat.buddy)) + else: + w_prnt("", "jabber: no server defined") + +def jabber_cmd_jabber(data, buffer, args): + """ Command '/jabber'. """ + global jabber_servers, jabber_config_option + if args == "" or args == "list": + jabber_list_servers_chats("") + else: + argv = args.split(" ") + argv1eol = "" + pos = args.find(" ") + if pos > 0: + argv1eol = args[pos+1:] + if argv[0] == "list": + jabber_list_servers_chats(argv[1]) + elif argv[0] == "add": + if len(argv) >= 4: + server = jabber_search_server_by_name(argv[1]) + if server: + w_prnt("", "jabber: server '%s' already exists" % argv[1]) + else: + kwargs = {'jid': argv[2], 'password': argv[3]} + if len(argv) > 4: + conn_server, _, conn_port = argv[4].partition(':') + if conn_port and not conn_port.isdigit(): + w_prnt("", "jabber: error, invalid port, digits only") + return w.WEECHAT_RC_OK + if conn_server: kwargs['server'] = conn_server + if conn_port: kwargs['port'] = conn_port + server = Server(argv[1], **kwargs) + jabber_servers.append(server) + w_prnt("", "jabber: server '%s' created" % argv[1]) + else: + w_prnt("", "jabber: unable to add server, missing arguments") + w_prnt("", "jabber: usage: /jabber add name jid password [server[:port]]") + elif argv[0] == "alias": + alias_command = AliasCommand(buffer, argv=argv[1:]) + alias_command.run() + + elif argv[0] == "connect": + server = None + if len(argv) >= 2: + server = jabber_search_server_by_name(argv[1]) + if not server: + w_prnt("", "jabber: server '%s' not found" % argv[1]) + else: + context = jabber_search_context(buffer) + if context["server"]: + server = context["server"] + if server: + if w.config_boolean(server.options['autoreconnect']): + server.ping() # This will connect and update ping status + server.add_ping_timer() + else: + server.connect() + + elif argv[0] == "disconnect": + server = None + if len(argv) >= 2: + server = jabber_search_server_by_name(argv[1]) + if not server: + w_prnt("", "jabber: server '%s' not found" % argv[1]) + else: + context = jabber_search_context(buffer) + if context["server"]: + server = context["server"] + context = jabber_search_context(buffer) + if server: + server.delete_ping_timer() + server.disconnect() + elif argv[0] == "del": + if len(argv) >= 2: + server = jabber_search_server_by_name(argv[1]) + if server: + server.delete(deleteOptions=True) + jabber_servers.remove(server) + w_prnt("", "jabber: server '%s' deleted" % argv[1]) + else: + w_prnt("", "jabber: server '%s' not found" % argv[1]) + elif argv[0] == "send": + if len(argv) >= 3: + context = jabber_search_context(buffer) + if context["server"]: + buddy = context['server'].search_buddy_list(argv[1], by='alias') + message = ' '.join(argv[2:]) + context["server"].send_message(buddy, message) + elif argv[0] == "read": + jabber_config_read() + elif argv[0] == "away": + context = jabber_search_context(buffer) + if context["server"]: + context["server"].set_away(argv1eol) + elif argv[0] == "priority": + context = jabber_search_context(buffer) + if context["server"]: + if len(argv) == 1: + w_prnt("", "jabber: priority = %d" % int(context["server"].presence.getPriority())) + elif len(argv) == 2 and argv[1].isdigit(): + context["server"].set_presence(priority=int(argv[1])) + else: + w_prnt("", "jabber: you need to specify priority as positive integer between 0 and 65535") + elif argv[0] == "status": + context = jabber_search_context(buffer) + if context["server"]: + if len(argv) == 1: + w_prnt("", "jabber: status = %s" % context["server"].presence.getStatus()) + else: + context["server"].set_presence(status=argv1eol) + elif argv[0] == "presence": + context = jabber_search_context(buffer) + if context["server"]: + if len(argv) == 1: + show = context["server"].presence.getShow() + if show == "": show = "online" + w_prnt("", "jabber: presence = %s" % show) + elif not re.match(r'^(?:online|chat|away|xa|dnd)$', argv[1]): + w_prnt("", "jabber: Presence should be one of: online, chat, away, xa, dnd") + else: + if argv[1] == "online": show = "" + else: show = argv[1] + context["server"].set_presence(show=show) + elif argv[0] == "buddies": + context = jabber_search_context(buffer) + if context["server"]: + context["server"].display_buddies() + + elif argv[0] == "debug": + w.config_option_set(jabber_config_option["debug"], "toggle", 1) + if jabber_debug_enabled(): + w_prnt("", "jabber: debug is now ON") + else: + w_prnt("", "jabber: debug is now off") + else: + w_prnt("", "jabber: unknown action") + return w.WEECHAT_RC_OK + +def jabber_cmd_jchat(data, buffer, args): + """ Command '/jchat'. """ + if args: + context = jabber_search_context(buffer) + if context["server"]: + buddy = context["server"].search_buddy_list(args, by='alias') + if not buddy: + buddy = context["server"].add_buddy(jid=args) + if not buddy.chat: + context["server"].add_chat(buddy) + w.buffer_set(buddy.chat.buffer, "display", "auto") + return w.WEECHAT_RC_OK + +def jabber_cmd_jmsg(data, buffer, args): + """ Command '/jmsg'. """ + if args: + argv = args.split() + if len(argv) < 2: + return w.WEECHAT_RC_OK + if argv[0] == '-server': + context = jabber_search_context_by_name(argv[1]) + recipient = argv[2] + message = " ".join(argv[3:]) + else: + context = jabber_search_context(buffer) + recipient = argv[0] + message = " ".join(argv[1:]) + if context["server"]: + buddy = context['server'].search_buddy_list(recipient, by='alias') + context["server"].send_message(buddy, message) + + return w.WEECHAT_RC_OK + +def jabber_cmd_invite(data, buffer, args): + """ Command '/invite'. """ + if args: + context = jabber_search_context(buffer) + if context["server"]: + context["server"].add_buddy(args) + return w.WEECHAT_RC_OK + +def jabber_cmd_kick(data, buffer, args): + """ Command '/kick'. """ + if args: + context = jabber_search_context(buffer) + if context["server"]: + context["server"].del_buddy(args) + return w.WEECHAT_RC_OK + +def jabber_away_command_run_cb(data, buffer, command): + """ Callback called when /away -all command is run """ + global jabber_servers + words = command.split(None, 2) + if len(words) < 2: + return + message = '' + if len(words) > 2: + message = words[2] + for server in jabber_servers: + server.set_away(message) + return w.WEECHAT_RC_OK + +class AliasCommand(object): + """Class representing a jabber alias command, ie /jabber alias ...""" + + def __init__(self, buffer, argv=None): + """ + Args: + bufffer: the weechat buffer the command was run in + argv: list, the arguments provided with the command. + Example, if the command is "/jabber alias add abc abc@server.tld" + argv = ['add', 'abc', 'abc@server.tld'] + """ + self.buffer = buffer + self.argv = [] + if argv: + self.argv = argv + self.action = '' + self.jid = '' + self.alias = '' + self.parse() + return + + def add(self): + """Run a "/jabber alias add" command""" + global jabber_jid_aliases + if not self.alias or not self.jid: + w_prnt("", "\njabber: unable to add alias, missing arguments") + w_prnt("", "jabber: usage: /jabber alias add alias_name jid") + return + # Restrict the character set of aliases. The characters must be writable to + # config file. + invalid_re = re.compile(r'[^a-zA-Z0-9\[\]\\\^_\-{|}@\.]') + if invalid_re.search(self.alias): + w_prnt("", "\njabber: invalid alias: %s" % self.alias) + w_prnt("", "jabber: use only characters: a-z A-Z 0-9 [ \ ] ^ _ - { | } @ .") + return + # Ensure alias and jid are reasonable length. + max_len = 64 + if len(self.alias) > max_len: + w_prnt("", "\njabber: invalid alias: %s" % self.alias) + w_prnt("", "jabber: must be no more than %s characters long" % max_len) + return + if len(self.jid) > max_len: + w_prnt("", "\njabber: invalid jid: %s" % self.jid) + w_prnt("", "jabber: must be no more than %s characters long" % max_len) + return + jid = self.jid.encode("utf-8") + alias = self.alias.encode("utf-8") + if alias in jabber_jid_aliases.keys(): + w_prnt("", "\njabber: unable to add alias: %s" % (alias)) + w_prnt("", "jabber: alias already exists, delete first") + return + if jid in jabber_jid_aliases.values(): + w_prnt("", "\njabber: unable to add alias: %s" % (alias)) + for a, j in jabber_jid_aliases.items(): + if j == jid: + w_prnt("", "jabber: jid '%s' is already aliased as '%s', delete first" % + (j, a)) + break + jabber_jid_aliases[alias] = jid + self.alias_reset(jid) + return + + def alias_reset(self, jid): + """Reset objects related to the jid modified by an an alias command + + Update any existing buddy objects, server nicklists, and chat objects + that may be using the buddy with the provided jid. + """ + global jabber_servers + for server in jabber_servers: + buddy = server.search_buddy_list(jid, by='jid') + if not buddy: + continue + server.update_nicklist(buddy=buddy, action='remove') + buddy.set_alias() + server.update_nicklist(buddy=buddy, action='update') + if buddy.chat: + switch_to_buffer = False + if buddy.chat.buffer == self.buffer: + switch_to_buffer = True + buddy.chat.delete() + new_chat = server.add_chat(buddy) + if switch_to_buffer: + w.buffer_set(new_chat.buffer, "display", "auto") + return + + def delete(self): + """Run a "/jabber alias del" command""" + global jabber_jid_aliases + if not self.alias: + w_prnt("", "\njabber: unable to delete alias, missing arguments") + w_prnt("", "jabber: usage: /jabber alias del alias_name") + return + if not self.alias in jabber_jid_aliases: + w_prnt("", "\njabber: unable to delete alias '%s', not found" % (self.alias)) + return + jid = jabber_jid_aliases[self.alias] + del jabber_jid_aliases[self.alias] + self.alias_reset(jid) + return + + def list(self): + """Run a "/jabber alias" command to list aliases""" + global jabber_jid_aliases + w_prnt("", "") + if len(jabber_jid_aliases) <= 0: + w_prnt("", "jabber: no aliases defined") + return + w_prnt("", "jabber jid aliases:") + len_alias = 5 + len_jid = 5 + for alias, jid in jabber_jid_aliases.items(): + if len_alias < len(alias): + len_alias = len(alias) + if len_jid < len(jid): + len_jid = len(jid) + prnt_format = " %-" + str(len_alias) + "s %-" + str(len_jid) + "s" + w_prnt("", prnt_format % ('Alias', 'JID')) + for alias, jid in sorted(jabber_jid_aliases.items()): + w_prnt("", prnt_format % (alias, jid)) + return + + def parse(self): + """Parse the alias command into components""" + if len(self.argv) <= 0: + return + self.action = self.argv[0] + if len(self.argv) > 1: + # Pad argv list to prevent IndexError exceptions + while len(self.argv) < 3: self.argv.append('') + self.alias = self.argv[1] + self.jid = self.argv[2] + return + + def run(self): + """Execute the alias command.""" + if self.action == 'add': + self.add() + elif self.action == 'del': + self.delete() + self.list() + return + +def jabber_completion_servers(data, completion_item, buffer, completion): + """ Completion with jabber server names. """ + global jabber_servers + for server in jabber_servers: + w.hook_completion_list_add(completion, server.name, + 0, w.WEECHAT_LIST_POS_SORT) + return w.WEECHAT_RC_OK + +def jabber_completion_jid_aliases(data, completion_item, buffer, completion): + """ Completion with jabber alias names. """ + global jabber_jid_aliases + for alias, jid in sorted(jabber_jid_aliases.items()): + w.hook_completion_list_add(completion, alias, + 0, w.WEECHAT_LIST_POS_SORT) + return w.WEECHAT_RC_OK + +# ==================================[ fd ]==================================== + +def jabber_fd_cb(data, fd): + """ Callback for reading socket. """ + global jabber_servers + for server in jabber_servers: + if server.sock == int(fd): + server.recv() + return w.WEECHAT_RC_OK + +# ================================[ buffers ]================================= + +def jabber_buffer_input_cb(data, buffer, input_data): + """ Callback called for input data on a jabber buffer. """ + context = jabber_search_context(buffer) + if context["server"] and context["chat"]: + context["chat"].send_message(input_data) + elif context["server"]: + if input_data == "buddies" or "buddies".startswith(input_data): + context["server"].display_buddies() + else: + context["server"].send_message_from_input(input=input_data) + return w.WEECHAT_RC_OK + +def jabber_buffer_close_cb(data, buffer): + """ Callback called when a jabber buffer is closed. """ + context = jabber_search_context(buffer) + if context["server"] and context["chat"]: + if context["chat"].buddy: + context["chat"].buddy.chat = None + context["chat"].buffer = "" + context["server"].chats.remove(context["chat"]) + elif context["server"]: + context["server"].buffer = "" + return w.WEECHAT_RC_OK + +# ==================================[ timers ]================================== + +def jabber_ping_timeout_timer(server_name, remaining_calls): + server = jabber_search_server_by_name(server_name) + if server: + server.ping_time_out() + return w.WEECHAT_RC_OK + +def jabber_ping_timer(server_name, remaining_calls): + server = jabber_search_server_by_name(server_name) + if server: + server.ping() + return w.WEECHAT_RC_OK + +# ==================================[ main ]================================== + +# ==================================[ end ]=================================== + +def jabber_unload_script(): + """ Function called when script is unloaded. """ + global jabber_servers + jabber_config_write() + for server in jabber_servers: + server.disconnect() + server.delete() + return w.WEECHAT_RC_OK + +if __name__ == "__main__": + # WeechatWrapper + w = weechat + + if w.register(SCRIPT_NAME, + SCRIPT_AUTHOR, + SCRIPT_VERSION, + SCRIPT_LICENSE, + SCRIPT_DESC, + "jabber_unload_script", ""): + + weechat_version = int(w.info_get("version_number", "") or 0) + jabber_hook_commands_and_completions() + jabber_config_init() + jabber_config_read() + for server in jabber_servers: + if w.config_boolean(server.options['autoreconnect']): + server.ping() # This will connect and update ping status + server.add_ping_timer() + else: + if w.config_boolean(server.options['autoconnect']): + server.connect() +