2264 lines
90 KiB
Python
2264 lines
90 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (C) 2009-2013 Sebastien Helleu <flashcode@flashtux.org>
|
|
# Copyright (C) 2010 xt <xt@bash.no>
|
|
# Copyright (C) 2010 Aleksey V. Zapparov <ixti@member.fsf.org>
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
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 <name> <jid> <password>
|
|
| [<server>[:<port>]]
|
|
connect|disconnect|del [<server>]
|
|
alias [add|del <alias> <jid>]
|
|
away [<message>]
|
|
buddies
|
|
priority [<priority>]
|
|
status [<message>]
|
|
presence | [online|chat|away|xa|dnd]
|
|
|
|
"""
|
|
# History:
|
|
#
|
|
|
|
SCRIPT_NAME = "jabber"
|
|
SCRIPT_AUTHOR = "Sebastien Helleu <flashcode@flashtux.org>"
|
|
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 <status/> 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 <name> <jid> <password> [<server>[:<port>]]"
|
|
" || connect|disconnect|del [<server>] || alias [add|del <alias> <jid>]"
|
|
" || away [<message>] || buddies || priority [<priority>]"
|
|
" || status [<message>] || presence [online|chat|away|xa|dnd]"
|
|
" || debug || set <server> <setting> [<value>]",
|
|
" 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: buddy id",
|
|
"",
|
|
"jabber_cmd_jchat", "")
|
|
w.hook_command("jmsg", "Send a messge to a buddy",
|
|
"[-server <server>] <buddy> <text>",
|
|
"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: buddy id",
|
|
"",
|
|
"jabber_cmd_invite", "")
|
|
w.hook_command("kick", "Remove a buddy from your roster, or deny auth",
|
|
"<buddy>",
|
|
"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()
|
|
|