mirror of
https://github.com/hexchat/hexchat.git
synced 2024-11-13 22:53:45 +01:00
387 lines
11 KiB
Python
387 lines
11 KiB
Python
import inspect
|
|
import sys
|
|
from contextlib import contextmanager
|
|
|
|
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] == 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 == 0:
|
|
return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs)
|
|
|
|
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
|
|
|
|
|
|
# TODO: this shadows itself. command should be changed to cmd
|
|
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 == 0:
|
|
return None
|
|
|
|
if _type == 1:
|
|
return __decode(ffi.string(string_out[0]))
|
|
|
|
if _type in (2, 3): # XXX: 3 should be a bool, but keeps API
|
|
return int_out[0]
|
|
|
|
raise AssertionError('Out of bounds pref storage')
|
|
|
|
|
|
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_) == 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
|
|
|
|
|
|
# TODO: 'command' here shadows command above, and should be renamed to cmd
|
|
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()))
|
|
|
|
if isinstance(value, int):
|
|
return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value))
|
|
|
|
# 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) != 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) == 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)
|