summary refs log blame commit diff stats
path: root/plugins/python/_hexchat.py
blob: 50ccfb8354e8b5857e84c8e0ab8e817360bc7980 (plain) (tree)























































































































































                                                                                               








                            






































                                                                             
                                                   











































































































































































                                                                                                      
from contextlib import contextmanager
import inspect
import sys
from _hexchat_embedded import ffi, lib

__all__ = [
    'EAT_ALL', 'EAT_HEXCHAT', 'EAT_NONE', 'EAT_PLUGIN', 'EAT_XCHAT',
    'PRI_HIGH', 'PRI_HIGHEST', 'PRI_LOW', 'PRI_LOWEST', 'PRI_NORM',
    '__doc__', '__version__', 'command', 'del_pluginpref', 'emit_print',
    'find_context', 'get_context', 'get_info',
    'get_list', 'get_lists', 'get_pluginpref', 'get_prefs', 'hook_command',
    'hook_print', 'hook_print_attrs', 'hook_server', 'hook_server_attrs',
    'hook_timer', 'hook_unload', 'list_pluginpref', 'nickcmp', 'prnt',
    'set_pluginpref', 'strip', 'unhook',
]

__doc__ = 'HexChat Scripting Interface'
__version__ = (2, 0)
__license__ = 'GPL-2.0+'

EAT_NONE = 0
EAT_HEXCHAT = 1
EAT_XCHAT = EAT_HEXCHAT
EAT_PLUGIN = 2
EAT_ALL = EAT_HEXCHAT | EAT_PLUGIN

PRI_LOWEST = -128
PRI_LOW = -64
PRI_NORM = 0
PRI_HIGH = 64
PRI_HIGHEST = 127


# We need each module to be able to reference their parent plugin
# which is a bit tricky since they all share the exact same module.
# Simply navigating up to what module called it seems to actually
# be a fairly reliable and simple method of doing so if ugly.
def __get_current_plugin():
    frame = inspect.stack()[1][0]
    while '__plugin' not in frame.f_globals:
        frame = frame.f_back
        assert frame is not None
    return frame.f_globals['__plugin']


# Keeping API compat
if sys.version_info[0] is 2:
    def __decode(string):
        return string
else:
    def __decode(string):
        return string.decode()


# ------------ API ------------
def prnt(string):
    lib.hexchat_print(lib.ph, string.encode())


def emit_print(event_name, *args, **kwargs):
    time = kwargs.pop('time', 0)  # For py2 compat
    cargs = []
    for i in range(4):
        arg = args[i].encode() if len(args) > i else b''
        cstring = ffi.new('char[]', arg)
        cargs.append(cstring)
    if time is 0:
        return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs)
    else:
        attrs = lib.hexchat_event_attrs_create(lib.ph)
        attrs.server_time_utc = time
        ret = lib.hexchat_emit_print(lib.ph, attrs, event_name.encode(), *cargs)
        lib.hexchat_event_attrs_free(lib.ph, attrs)
        return ret


def command(command):
    lib.hexchat_command(lib.ph, command.encode())


def nickcmp(string1, string2):
    return lib.hexchat_nickcmp(lib.ph, string1.encode(), string2.encode())


def strip(text, length=-1, flags=3):
    stripped = lib.hexchat_strip(lib.ph, text.encode(), length, flags)
    ret = __decode(ffi.string(stripped))
    lib.hexchat_free(lib.ph, stripped)
    return ret


def get_info(name):
    ret = lib.hexchat_get_info(lib.ph, name.encode())
    if ret == ffi.NULL:
        return None
    if name in ('gtkwin_ptr', 'win_ptr'):
        # Surely there is a less dumb way?
        ptr = repr(ret).rsplit(' ', 1)[1][:-1]
        return ptr
    return __decode(ffi.string(ret))


def get_prefs(name):
    string_out = ffi.new('char**')
    int_out = ffi.new('int*')
    type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out)
    if type is 0:
        return None
    elif type is 1:
        return __decode(ffi.string(string_out[0]))
    elif type is 2 or type is 3:  # XXX: 3 should be a bool, but keeps API
        return int_out[0]
    else:
        assert False


def __cstrarray_to_list(arr):
    i = 0
    ret = []
    while arr[i] != ffi.NULL:
        ret.append(ffi.string(arr[i]))
        i += 1
    return ret


__FIELD_CACHE = {}


def __get_fields(name):
    return __FIELD_CACHE.setdefault(name,
                                    __cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name)))


__FIELD_PROPERTY_CACHE = {}


def __cached_decoded_str(string):
    return __FIELD_PROPERTY_CACHE.setdefault(string, __decode(string))


def get_lists():
    return [__cached_decoded_str(field) for field in __get_fields(b'lists')]


class ListItem:
    def __init__(self, name):
        self._listname = name

    def __repr__(self):
        return '<{} list item at {}>'.format(self._listname, id(self))


# done this way for speed
if sys.version_info[0] == 2:
    def get_getter(name):
        return ord(name[0])
else:
    def get_getter(name):
        return name[0]


def get_list(name):
    # XXX: This function is extremely inefficient and could be interators and
    # lazily loaded properties, but for API compat we stay slow
    orig_name = name
    name = name.encode()

    if name not in __get_fields(b'lists'):
        raise KeyError('list not available')

    list_ = lib.hexchat_list_get(lib.ph, name)
    if list_ == ffi.NULL:
        return None

    ret = []
    fields = __get_fields(name)

    def string_getter(field):
        string = lib.hexchat_list_str(lib.ph, list_, field)
        if string != ffi.NULL:
            return __decode(ffi.string(string))
        return ''

    def ptr_getter(field):
        if field == b'context':
            ptr = lib.hexchat_list_str(lib.ph, list_, field)
            ctx = ffi.cast('hexchat_context*', ptr)
            return Context(ctx)
        return None

    getters = {
        ord('s'): string_getter,
        ord('i'): lambda field: lib.hexchat_list_int(lib.ph, list_, field),
        ord('t'): lambda field: lib.hexchat_list_time(lib.ph, list_, field),
        ord('p'): ptr_getter,
    }

    while lib.hexchat_list_next(lib.ph, list_) is 1:
        item = ListItem(orig_name)
        for field in fields:
            getter = getters.get(get_getter(field))
            if getter is not None:
                field_name = field[1:]
                setattr(item, __cached_decoded_str(field_name), getter(field_name))
        ret.append(item)

    lib.hexchat_list_free(lib.ph, list_)
    return ret


def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None):
    plugin = __get_current_plugin()
    hook = plugin.add_hook(callback, userdata)
    handle = lib.hexchat_hook_command(lib.ph, command.encode(), priority, lib._on_command_hook,
                                      help.encode() if help is not None else ffi.NULL,
                                      hook.handle)
    hook.hexchat_hook = handle
    return id(hook)


def hook_print(name, callback, userdata=None, priority=PRI_NORM):
    plugin = __get_current_plugin()
    hook = plugin.add_hook(callback, userdata)
    handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook,
                                    hook.handle)
    hook.hexchat_hook = handle
    return id(hook)


def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM):
    plugin = __get_current_plugin()
    hook = plugin.add_hook(callback, userdata)
    handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook,
                                          hook.handle)
    hook.hexchat_hook = handle
    return id(hook)


def hook_server(name, callback, userdata=None, priority=PRI_NORM):
    plugin = __get_current_plugin()
    hook = plugin.add_hook(callback, userdata)
    handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook,
                                     hook.handle)
    hook.hexchat_hook = handle
    return id(hook)


def hook_server_attrs(name, callback, userdata=None, priority=PRI_NORM):
    plugin = __get_current_plugin()
    hook = plugin.add_hook(callback, userdata)
    handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook,
                                           hook.handle)
    hook.hexchat_hook = handle
    return id(hook)


def hook_timer(timeout, callback, userdata=None):
    plugin = __get_current_plugin()
    hook = plugin.add_hook(callback, userdata)
    handle = lib.hexchat_hook_timer(lib.ph, timeout, lib._on_timer_hook, hook.handle)
    hook.hexchat_hook = handle
    return id(hook)


def hook_unload(callback, userdata=None):
    plugin = __get_current_plugin()
    hook = plugin.add_hook(callback, userdata, is_unload=True)
    return id(hook)


def unhook(handle):
    plugin = __get_current_plugin()
    return plugin.remove_hook(handle)


def set_pluginpref(name, value):
    if isinstance(value, str):
        return bool(lib.hexchat_pluginpref_set_str(lib.ph, name.encode(), value.encode()))
    elif isinstance(value, int):
        return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value))
    else:
        # XXX: This should probably raise but this keeps API
        return False


def get_pluginpref(name):
    name = name.encode()
    string_out = ffi.new('char[512]')
    if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) is not 1:
        return None

    string = ffi.string(string_out)
    # This API stores everything as a string so we have to figure out what
    # its actual type was supposed to be.
    if len(string) > 12:  # Can't be a number
        return __decode(string)

    number = lib.hexchat_pluginpref_get_int(lib.ph, name)
    if number == -1 and string != b'-1':
        return __decode(string)

    return number


def del_pluginpref(name):
    return bool(lib.hexchat_pluginpref_delete(lib.ph, name.encode()))


def list_pluginpref():
    prefs_str = ffi.new('char[4096]')
    if lib.hexchat_pluginpref_list(lib.ph, prefs_str) is 1:
        return __decode(prefs_str).split(',')
    return []


class Context:
    def __init__(self, ctx):
        self._ctx = ctx

    def __eq__(self, value):
        if not isinstance(value, Context):
            return False
        return self._ctx == value._ctx

    @contextmanager
    def __change_context(self):
        old_ctx = lib.hexchat_get_context(lib.ph)
        if not self.set():
            # XXX: Behavior change, previously used wrong context
            lib.hexchat_print(lib.ph,
                              b'Context object refers to closed context, ignoring call')
            return
        yield
        lib.hexchat_set_context(lib.ph, old_ctx)

    def set(self):
        # XXX: API addition, C plugin silently ignored failure
        return bool(lib.hexchat_set_context(lib.ph, self._ctx))

    def prnt(self, string):
        with self.__change_context():
            prnt(string)

    def emit_print(self, event_name, *args, **kwargs):
        time = kwargs.pop('time', 0)  # For py2 compat
        with self.__change_context():
            return emit_print(event_name, *args, time=time)

    def command(self, string):
        with self.__change_context():
            command(string)

    def get_info(self, name):
        with self.__change_context():
            return get_info(name)

    def get_list(self, name):
        with self.__change_context():
            return get_list(name)


def get_context():
    ctx = lib.hexchat_get_context(lib.ph)
    return Context(ctx)


def find_context(server=None, channel=None):
    server = server.encode() if server is not None else ffi.NULL
    channel = channel.encode() if channel is not None else ffi.NULL
    ctx = lib.hexchat_find_context(lib.ph, server, channel)
    if ctx == ffi.NULL:
        return None
    return Context(ctx)