Commit 8f0cb0e0 authored by Cool Fire's avatar Cool Fire

Merge remote-tracking branch 'upstream/master'

parents f84636cd 586f089d
Pipeline #1594 failed with stage
in 10 minutes and 17 seconds
- python3-cffi
......@@ -2,7 +2,7 @@ sudo: required
services: docker
- docker pull ubuntu:16.04
- docker run --privileged --cidfile=/tmp/cid ubuntu:16.04 /bin/sh -c 'apt-get update && apt-get install -y meson/xenial-backports libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libnotify-dev libpci-dev libperl-dev libproxy-dev libssl-dev python3-dev mono-devel desktop-file-utils'
- docker run --privileged --cidfile=/tmp/cid ubuntu:16.04 /bin/sh -c 'apt-get update && apt-get install -y meson/xenial-backports libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libnotify-dev libpci-dev libperl-dev libproxy-dev libssl-dev python3-dev python3-cffi mono-devel desktop-file-utils'
- docker commit `cat /tmp/cid` hexchat/ubuntu-ci
- rm -f /tmp/cid
......@@ -49,6 +49,10 @@ config_h.set('GLIB_VERSION_MIN_REQUIRED', 'GLIB_VERSION_2_34')
config_h.set('HAVE_MEMRCHR', cc.has_function('memrchr'))
config_h.set('HAVE_STRINGS_H', cc.has_header('strings.h'))
join_paths(get_option('prefix'), get_option('libdir'), 'hexchat/plugins')
if libssl_dep.found()
cc.has_function('X509_get_signature_nid', dependencies: libssl_dep)
import inspect
import sys
from contextlib import contextmanager
from _hexchat_embedded import ffi, lib
__all__ = [
'__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+'
PRI_LOW = -64
# 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
def __decode(string):
return string.decode()
# ------------ API ------------
def prnt(string):
lib.hexchat_print(, 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 ='char[]', arg)
if time == 0:
return lib.hexchat_emit_print(, event_name.encode(), *cargs)
attrs = lib.hexchat_event_attrs_create(
attrs.server_time_utc = time
ret = lib.hexchat_emit_print_attrs(, attrs, event_name.encode(), *cargs)
lib.hexchat_event_attrs_free(, attrs)
return ret
# TODO: this shadows itself. command should be changed to cmd
def command(command):
lib.hexchat_command(, command.encode())
def nickcmp(string1, string2):
return lib.hexchat_nickcmp(, string1.encode(), string2.encode())
def strip(text, length=-1, flags=3):
stripped = lib.hexchat_strip(, text.encode(), length, flags)
ret = __decode(ffi.string(stripped))
lib.hexchat_free(, stripped)
return ret
def get_info(name):
ret = lib.hexchat_get_info(, 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 ='char**')
int_out ='int*')
_type = lib.hexchat_get_prefs(, 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:
i += 1
return ret
def __get_fields(name):
return __FIELD_CACHE.setdefault(name, __cstrarray_to_list(lib.hexchat_list_fields(, name)))
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])
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(, name)
if list_ == ffi.NULL:
return None
ret = []
fields = __get_fields(name)
def string_getter(field):
string = lib.hexchat_list_str(, 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(, 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(, list_, field),
ord('t'): lambda field: lib.hexchat_list_time(, list_, field),
ord('p'): ptr_getter,
while lib.hexchat_list_next(, 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))
lib.hexchat_list_free(, 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(, 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(, 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(, 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(, 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(, 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(, 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(, name.encode(), value.encode()))
if isinstance(value, int):
return bool(lib.hexchat_pluginpref_set_int(, name.encode(), value))
# XXX: This should probably raise but this keeps API
return False
def get_pluginpref(name):
name = name.encode()
string_out ='char[512]')
if lib.hexchat_pluginpref_get_str(, 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(, name)
if number == -1 and string != b'-1':
return __decode(string)
return number
def del_pluginpref(name):
return bool(lib.hexchat_pluginpref_delete(, name.encode()))
def list_pluginpref():
prefs_str ='char[4096]')
if lib.hexchat_pluginpref_list(, 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
def __change_context(self):
old_ctx = lib.hexchat_get_context(
if not self.set():
# XXX: Behavior change, previously used wrong context
lib.hexchat_print(, b'Context object refers to closed context, ignoring call')
lib.hexchat_set_context(, old_ctx)
def set(self):
# XXX: API addition, C plugin silently ignored failure
return bool(lib.hexchat_set_context(, self._ctx))
def prnt(self, string):
with self.__change_context():
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():
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(
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(, server, channel)
if ctx == ffi.NULL:
return None
return Context(ctx)
#!/usr/bin/env python3
import sys
import cffi
builder = cffi.FFI()
# hexchat-plugin.h
with open(sys.argv[1]) as f:
output = []
eat_until_endif = 0
# This is very specific to hexchat-plugin.h, it is not a cpp
for line in f:
if line.startswith('#define'):
elif line.endswith('HEXCHAT_PLUGIN_H\n'):
elif 'time.h' in line:
output.append('typedef int... time_t;')
elif line.startswith('#if'):
eat_until_endif += 1
elif line.startswith('#endif'):
eat_until_endif -= 1
elif eat_until_endif and '_hexchat_context' not in line:
extern "Python" int _on_py_command(char **, char **, void *);
extern "Python" int _on_load_command(char **, char **, void *);
extern "Python" int _on_unload_command(char **, char **, void *);
extern "Python" int _on_reload_command(char **, char **, void *);
extern "Python" int _on_say_command(char **, char **, void *);
extern "Python" int _on_command_hook(char **, char **, void *);
extern "Python" int _on_print_hook(char **, void *);
extern "Python" int _on_print_attrs_hook(char **, hexchat_event_attrs *, void *);
extern "Python" int _on_server_hook(char **, char **, void *);
extern "Python" int _on_server_attrs_hook(char **, char **, hexchat_event_attrs *, void *);
extern "Python" int _on_timer_hook(void *);
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
extern "Python" int _on_plugin_deinit(void);
static hexchat_plugin *ph;
builder.set_source('_hexchat_embedded', '''
/* Python's header defines these.. */
#include "config.h"
#include "hexchat-plugin.h"
static hexchat_plugin *ph;
CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *);
CFFI_DLLEXPORT int _on_plugin_deinit(void);
int hexchat_plugin_init(hexchat_plugin *plugin_handle,
char **name_out, char **description_out,
char **version_out, char *arg)
if (ph != NULL)
puts ("Python plugin already loaded\\n");
return 0; /* Prevent loading twice */
ph = plugin_handle;
return _on_plugin_init(name_out, description_out, version_out, arg, HEXCHATLIBDIR);
int hexchat_plugin_deinit(void)
int ret = _on_plugin_deinit();
ph = NULL;
return ret;
with open(sys.argv[2]) as f:
# python.c
from _hexchat import *
......@@ -5,8 +5,18 @@ else
python_dep = dependency(python_opt, version: '>= 2.7')
shared_module('python', 'python.c',
dependencies: [libgio_dep, hexchat_plugin_dep, python_dep],
python3_source = custom_target('python-bindings',
input: ['../../src/common/hexchat-plugin.h', ''],
output: 'python.c',
command: [find_program(''), '@INPUT@', '@OUTPUT@']
install_data(['', '', ''],
install_dir: join_paths(get_option('libdir'), 'hexchat/python')
shared_module('python', python3_source,
dependencies: [hexchat_plugin_dep, python_dep],
install: true,
install_dir: plugindir,
name_prefix: '',
This diff is collapsed.
from __future__ import print_function
import importlib
import os
import pydoc
import signal
import sys
import traceback
import weakref
from contextlib import contextmanager
from _hexchat_embedded import ffi, lib
if sys.version_info < (3, 0):
from io import BytesIO as HelpEater
from io import StringIO as HelpEater
if not hasattr(sys, 'argv'):
sys.argv = ['<hexchat>']
VERSION = b'2.0' # Sync with hexchat.__version__
PLUGIN_NAME ='char[]', b'Python')
PLUGIN_DESC ='char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1]))
# TODO: Constants should be screaming snake case
hexchat = None
local_interp = None
hexchat_stdout = None
plugins = set()
def redirected_stdout():
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
sys.stdout = hexchat_stdout
sys.stderr = hexchat_stdout
if os.getenv('HEXCHAT_LOG_PYTHON'):
def log(*args):
with redirected_stdout():
def log(*args):
class Stdout:
def __init__(self):
self.buffer = bytearray()
def write(self, string):
string = string.encode()
idx = string.rfind(b'\n')
if idx != -1:
self.buffer += string[:idx]
lib.hexchat_print(, bytes(self.buffer))
self.buffer = bytearray(string[idx + 1:])
self.buffer += string
def isatty(self):
return False
class Attribute:
def __init__(self):
self.time = 0
def __repr__(self):
return '<Attribute object at {}>'.format(id(self))
class Hook:
def __init__(self, plugin, callback, userdata, is_unload):
self.is_unload = is_unload
self.plugin = weakref.proxy(plugin)
self.callback = callback
self.userdata = userdata
self.hexchat_hook = None
self.handle = ffi.new_handle(weakref.proxy(self))
def __del__(self):
log('Removing hook', id(self))
if self.is_unload is False:
assert self.hexchat_hook is not None
lib.hexchat_unhook(, self.hexchat_hook)