Python: Add support for attributes in hooks
This commit is contained in:
parent
2e2f4662f3
commit
f45487e92c
@ -211,7 +211,8 @@ static PyThreadState *pTempThread;
|
|||||||
((PluginObject *)(x))->gui = (y);
|
((PluginObject *)(x))->gui = (y);
|
||||||
|
|
||||||
#define HOOK_XCHAT 1
|
#define HOOK_XCHAT 1
|
||||||
#define HOOK_UNLOAD 2
|
#define HOOK_XCHAT_ATTR 2
|
||||||
|
#define HOOK_UNLOAD 3
|
||||||
|
|
||||||
/* ===================================================================== */
|
/* ===================================================================== */
|
||||||
/* Object definitions */
|
/* Object definitions */
|
||||||
@ -226,6 +227,11 @@ typedef struct {
|
|||||||
hexchat_context *context;
|
hexchat_context *context;
|
||||||
} ContextObject;
|
} ContextObject;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD
|
||||||
|
PyObject *time;
|
||||||
|
} AttributeObject;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
const char *listname;
|
const char *listname;
|
||||||
@ -261,8 +267,9 @@ static PyObject *Util_BuildList(char *word[]);
|
|||||||
static void Util_Autoload();
|
static void Util_Autoload();
|
||||||
static char *Util_Expand(char *filename);
|
static char *Util_Expand(char *filename);
|
||||||
|
|
||||||
|
static int Callback_Server(char *word[], char *word_eol[], hexchat_event_attrs *attrs, void *userdata);
|
||||||
static int Callback_Command(char *word[], char *word_eol[], void *userdata);
|
static int Callback_Command(char *word[], char *word_eol[], void *userdata);
|
||||||
static int Callback_Print(char *word[], void *userdata);
|
static int Callback_Print(char *word[], hexchat_event_attrs *attrs, void *userdata);
|
||||||
static int Callback_Timer(void *userdata);
|
static int Callback_Timer(void *userdata);
|
||||||
static int Callback_ThreadTimer(void *userdata);
|
static int Callback_ThreadTimer(void *userdata);
|
||||||
|
|
||||||
@ -270,6 +277,8 @@ static PyObject *XChatOut_New();
|
|||||||
static PyObject *XChatOut_write(PyObject *self, PyObject *args);
|
static PyObject *XChatOut_write(PyObject *self, PyObject *args);
|
||||||
static void XChatOut_dealloc(PyObject *self);
|
static void XChatOut_dealloc(PyObject *self);
|
||||||
|
|
||||||
|
static PyObject *Attribute_New(hexchat_event_attrs *attrs);
|
||||||
|
|
||||||
static void Context_dealloc(PyObject *self);
|
static void Context_dealloc(PyObject *self);
|
||||||
static PyObject *Context_set(ContextObject *self, PyObject *args);
|
static PyObject *Context_set(ContextObject *self, PyObject *args);
|
||||||
static PyObject *Context_command(ContextObject *self, PyObject *args);
|
static PyObject *Context_command(ContextObject *self, PyObject *args);
|
||||||
@ -331,6 +340,7 @@ static PyTypeObject Plugin_Type;
|
|||||||
static PyTypeObject XChatOut_Type;
|
static PyTypeObject XChatOut_Type;
|
||||||
static PyTypeObject Context_Type;
|
static PyTypeObject Context_Type;
|
||||||
static PyTypeObject ListItem_Type;
|
static PyTypeObject ListItem_Type;
|
||||||
|
static PyTypeObject Attribute_Type;
|
||||||
|
|
||||||
static PyThreadState *main_tstate = NULL;
|
static PyThreadState *main_tstate = NULL;
|
||||||
static void *thread_timer = NULL;
|
static void *thread_timer = NULL;
|
||||||
@ -487,6 +497,58 @@ Util_ReleaseThread(PyThreadState *tstate)
|
|||||||
/* Hookable functions. These are the entry points to python code, besides
|
/* Hookable functions. These are the entry points to python code, besides
|
||||||
* the load function, and the hooks for interactive interpreter. */
|
* the load function, and the hooks for interactive interpreter. */
|
||||||
|
|
||||||
|
static int
|
||||||
|
Callback_Server(char *word[], char *word_eol[], hexchat_event_attrs *attrs, void *userdata)
|
||||||
|
{
|
||||||
|
Hook *hook = (Hook *) userdata;
|
||||||
|
PyObject *retobj;
|
||||||
|
PyObject *word_list, *word_eol_list;
|
||||||
|
PyObject *attributes;
|
||||||
|
int ret = 0;
|
||||||
|
PyObject *plugin;
|
||||||
|
|
||||||
|
plugin = hook->plugin;
|
||||||
|
BEGIN_PLUGIN(plugin);
|
||||||
|
|
||||||
|
word_list = Util_BuildList(word+1);
|
||||||
|
if (word_list == NULL) {
|
||||||
|
END_PLUGIN(plugin);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
word_eol_list = Util_BuildList(word_eol+1);
|
||||||
|
if (word_eol_list == NULL) {
|
||||||
|
Py_DECREF(word_list);
|
||||||
|
END_PLUGIN(plugin);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes = Attribute_New(attrs);
|
||||||
|
|
||||||
|
if (hook->type == HOOK_XCHAT_ATTR)
|
||||||
|
retobj = PyObject_CallFunction(hook->callback, "(OOOO)", word_list,
|
||||||
|
word_eol_list, hook->userdata, attributes);
|
||||||
|
else
|
||||||
|
retobj = PyObject_CallFunction(hook->callback, "(OOO)", word_list,
|
||||||
|
word_eol_list, hook->userdata);
|
||||||
|
Py_DECREF(word_list);
|
||||||
|
Py_DECREF(word_eol_list);
|
||||||
|
Py_DECREF(attributes);
|
||||||
|
|
||||||
|
if (retobj == Py_None) {
|
||||||
|
ret = HEXCHAT_EAT_NONE;
|
||||||
|
Py_DECREF(retobj);
|
||||||
|
} else if (retobj) {
|
||||||
|
ret = PyLong_AsLong(retobj);
|
||||||
|
Py_DECREF(retobj);
|
||||||
|
} else {
|
||||||
|
PyErr_Print();
|
||||||
|
}
|
||||||
|
|
||||||
|
END_PLUGIN(plugin);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
Callback_Command(char *word[], char *word_eol[], void *userdata)
|
Callback_Command(char *word[], char *word_eol[], void *userdata)
|
||||||
{
|
{
|
||||||
@ -534,12 +596,13 @@ Callback_Command(char *word[], char *word_eol[], void *userdata)
|
|||||||
/* No Callback_Server() here. We use Callback_Command() as well. */
|
/* No Callback_Server() here. We use Callback_Command() as well. */
|
||||||
|
|
||||||
static int
|
static int
|
||||||
Callback_Print(char *word[], void *userdata)
|
Callback_Print(char *word[], hexchat_event_attrs *attrs, void *userdata)
|
||||||
{
|
{
|
||||||
Hook *hook = (Hook *) userdata;
|
Hook *hook = (Hook *) userdata;
|
||||||
PyObject *retobj;
|
PyObject *retobj;
|
||||||
PyObject *word_list;
|
PyObject *word_list;
|
||||||
PyObject *word_eol_list;
|
PyObject *word_eol_list;
|
||||||
|
PyObject *attributes;
|
||||||
char **word_eol;
|
char **word_eol;
|
||||||
char *word_eol_raw;
|
char *word_eol_raw;
|
||||||
int listsize = 0;
|
int listsize = 0;
|
||||||
@ -597,10 +660,18 @@ Callback_Print(char *word[], void *userdata)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
retobj = PyObject_CallFunction(hook->callback, "(OOO)", word_list,
|
attributes = Attribute_New(attrs);
|
||||||
word_eol_list, hook->userdata);
|
|
||||||
|
if (hook->type == HOOK_XCHAT_ATTR)
|
||||||
|
retobj = PyObject_CallFunction(hook->callback, "(OOOO)", word_list,
|
||||||
|
word_eol_list, hook->userdata, attributes);
|
||||||
|
else
|
||||||
|
retobj = PyObject_CallFunction(hook->callback, "(OOO)", word_list,
|
||||||
|
word_eol_list, hook->userdata);
|
||||||
|
|
||||||
Py_DECREF(word_list);
|
Py_DECREF(word_list);
|
||||||
Py_DECREF(word_eol_list);
|
Py_DECREF(word_eol_list);
|
||||||
|
Py_DECREF(attributes);
|
||||||
|
|
||||||
g_free(word_eol_raw);
|
g_free(word_eol_raw);
|
||||||
g_free(word_eol);
|
g_free(word_eol);
|
||||||
@ -822,6 +893,84 @@ static PyTypeObject XChatOut_Type = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* ===================================================================== */
|
||||||
|
/* Attribute object */
|
||||||
|
|
||||||
|
#define OFF(x) offsetof(AttributeObject, x)
|
||||||
|
|
||||||
|
static PyMemberDef Attribute_members[] = {
|
||||||
|
{"time", T_OBJECT, OFF(time), 0},
|
||||||
|
{0}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
Attribute_dealloc(PyObject *self)
|
||||||
|
{
|
||||||
|
Py_DECREF(((AttributeObject*)self)->time);
|
||||||
|
Py_TYPE(self)->tp_free((PyObject *)self);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
Attribute_repr(PyObject *self)
|
||||||
|
{
|
||||||
|
return PyUnicode_FromFormat("<Attribute object at %p>", self);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyTypeObject Attribute_Type = {
|
||||||
|
PyVarObject_HEAD_INIT(NULL, 0)
|
||||||
|
"hexchat.Attribute", /*tp_name*/
|
||||||
|
sizeof(AttributeObject), /*tp_basicsize*/
|
||||||
|
0, /*tp_itemsize*/
|
||||||
|
Attribute_dealloc, /*tp_dealloc*/
|
||||||
|
0, /*tp_print*/
|
||||||
|
0, /*tp_getattr*/
|
||||||
|
0, /*tp_setattr*/
|
||||||
|
0, /*tp_compare*/
|
||||||
|
Attribute_repr, /*tp_repr*/
|
||||||
|
0, /*tp_as_number*/
|
||||||
|
0, /*tp_as_sequence*/
|
||||||
|
0, /*tp_as_mapping*/
|
||||||
|
0, /*tp_hash*/
|
||||||
|
0, /*tp_call*/
|
||||||
|
0, /*tp_str*/
|
||||||
|
PyObject_GenericGetAttr,/*tp_getattro*/
|
||||||
|
PyObject_GenericSetAttr,/*tp_setattro*/
|
||||||
|
0, /*tp_as_buffer*/
|
||||||
|
Py_TPFLAGS_DEFAULT, /*tp_flags*/
|
||||||
|
0, /*tp_doc*/
|
||||||
|
0, /*tp_traverse*/
|
||||||
|
0, /*tp_clear*/
|
||||||
|
0, /*tp_richcompare*/
|
||||||
|
0, /*tp_weaklistoffset*/
|
||||||
|
0, /*tp_iter*/
|
||||||
|
0, /*tp_iternext*/
|
||||||
|
0, /*tp_methods*/
|
||||||
|
Attribute_members, /*tp_members*/
|
||||||
|
0, /*tp_getset*/
|
||||||
|
0, /*tp_base*/
|
||||||
|
0, /*tp_dict*/
|
||||||
|
0, /*tp_descr_get*/
|
||||||
|
0, /*tp_descr_set*/
|
||||||
|
0, /*tp_dictoffset*/
|
||||||
|
0, /*tp_init*/
|
||||||
|
PyType_GenericAlloc, /*tp_alloc*/
|
||||||
|
PyType_GenericNew, /*tp_new*/
|
||||||
|
PyObject_Del, /*tp_free*/
|
||||||
|
0, /*tp_is_gc*/
|
||||||
|
};
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
Attribute_New(hexchat_event_attrs *attrs)
|
||||||
|
{
|
||||||
|
AttributeObject *attr;
|
||||||
|
attr = PyObject_New(AttributeObject, &Attribute_Type);
|
||||||
|
if (attr != NULL) {
|
||||||
|
attr->time = PyLong_FromLong((long)attrs->server_time_utc);
|
||||||
|
}
|
||||||
|
return (PyObject *) attr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ===================================================================== */
|
/* ===================================================================== */
|
||||||
/* Context object */
|
/* Context object */
|
||||||
|
|
||||||
@ -1232,7 +1381,7 @@ Plugin_RemoveHook(PyObject *plugin, Hook *hook)
|
|||||||
list = g_slist_find(Plugin_GetHooks(plugin), hook);
|
list = g_slist_find(Plugin_GetHooks(plugin), hook);
|
||||||
if (list) {
|
if (list) {
|
||||||
/* Ok, unhook it. */
|
/* Ok, unhook it. */
|
||||||
if (hook->type == HOOK_XCHAT) {
|
if (hook->type != HOOK_UNLOAD) {
|
||||||
/* This is an xchat hook. Unregister it. */
|
/* This is an xchat hook. Unregister it. */
|
||||||
BEGIN_XCHAT_CALLS(NONE);
|
BEGIN_XCHAT_CALLS(NONE);
|
||||||
hexchat_unhook(ph, (hexchat_hook*)hook->data);
|
hexchat_unhook(ph, (hexchat_hook*)hook->data);
|
||||||
@ -1255,7 +1404,7 @@ Plugin_RemoveAllHooks(PyObject *plugin)
|
|||||||
GSList *list = Plugin_GetHooks(plugin);
|
GSList *list = Plugin_GetHooks(plugin);
|
||||||
while (list) {
|
while (list) {
|
||||||
Hook *hook = (Hook *) list->data;
|
Hook *hook = (Hook *) list->data;
|
||||||
if (hook->type == HOOK_XCHAT) {
|
if (hook->type != HOOK_UNLOAD) {
|
||||||
/* This is an xchat hook. Unregister it. */
|
/* This is an xchat hook. Unregister it. */
|
||||||
BEGIN_XCHAT_CALLS(NONE);
|
BEGIN_XCHAT_CALLS(NONE);
|
||||||
hexchat_unhook(ph, (hexchat_hook*)hook->data);
|
hexchat_unhook(ph, (hexchat_hook*)hook->data);
|
||||||
@ -1530,6 +1679,33 @@ Module_hexchat_emit_print(PyObject *self, PyObject *args)
|
|||||||
return PyLong_FromLong(res);
|
return PyLong_FromLong(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
Module_hexchat_emit_print_at(PyObject *self, PyObject *args)
|
||||||
|
{
|
||||||
|
char *argv[10];
|
||||||
|
char *name;
|
||||||
|
long time;
|
||||||
|
int res;
|
||||||
|
hexchat_event_attrs* attrs;
|
||||||
|
memset(&argv, 0, sizeof(char*)*10);
|
||||||
|
if (!PyArg_ParseTuple(args, "ls|ssssss:print_event_at", &time, &name,
|
||||||
|
&argv[0], &argv[1], &argv[2],
|
||||||
|
&argv[3], &argv[4], &argv[5],
|
||||||
|
&argv[6], &argv[7], &argv[8]))
|
||||||
|
return NULL;
|
||||||
|
BEGIN_XCHAT_CALLS(RESTORE_CONTEXT|ALLOW_THREADS);
|
||||||
|
attrs = hexchat_event_attrs_create(ph);
|
||||||
|
attrs->server_time_utc = (time_t)time;
|
||||||
|
|
||||||
|
res = hexchat_emit_print_attrs(ph, attrs, name, argv[0], argv[1], argv[2],
|
||||||
|
argv[3], argv[4], argv[5],
|
||||||
|
argv[6], argv[7], argv[8]);
|
||||||
|
|
||||||
|
hexchat_event_attrs_free(ph, attrs);
|
||||||
|
END_XCHAT_CALLS();
|
||||||
|
return PyLong_FromLong(res);
|
||||||
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
Module_hexchat_get_info(PyObject *self, PyObject *args)
|
Module_hexchat_get_info(PyObject *self, PyObject *args)
|
||||||
{
|
{
|
||||||
@ -1789,8 +1965,44 @@ Module_hexchat_hook_server(PyObject *self, PyObject *args, PyObject *kwargs)
|
|||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
BEGIN_XCHAT_CALLS(NONE);
|
BEGIN_XCHAT_CALLS(NONE);
|
||||||
hook->data = (void*)hexchat_hook_server(ph, name, priority,
|
hook->data = (void*)hexchat_hook_server_attrs(ph, name, priority,
|
||||||
Callback_Command, hook);
|
Callback_Server, hook);
|
||||||
|
END_XCHAT_CALLS();
|
||||||
|
|
||||||
|
return PyLong_FromLong((long)hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
Module_hexchat_hook_server_attrs(PyObject *self, PyObject *args, PyObject *kwargs)
|
||||||
|
{
|
||||||
|
char *name;
|
||||||
|
PyObject *callback;
|
||||||
|
PyObject *userdata = Py_None;
|
||||||
|
int priority = HEXCHAT_PRI_NORM;
|
||||||
|
PyObject *plugin;
|
||||||
|
Hook *hook;
|
||||||
|
char *kwlist[] = {"name", "callback", "userdata", "priority", 0};
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|Oi:hook_server",
|
||||||
|
kwlist, &name, &callback, &userdata,
|
||||||
|
&priority))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
plugin = Plugin_GetCurrent();
|
||||||
|
if (plugin == NULL)
|
||||||
|
return NULL;
|
||||||
|
if (!PyCallable_Check(callback)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "callback is not callable");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook = Plugin_AddHook(HOOK_XCHAT_ATTR, plugin, callback, userdata, NULL, NULL);
|
||||||
|
if (hook == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
BEGIN_XCHAT_CALLS(NONE);
|
||||||
|
hook->data = (void*)hexchat_hook_server_attrs(ph, name, priority,
|
||||||
|
Callback_Server, hook);
|
||||||
END_XCHAT_CALLS();
|
END_XCHAT_CALLS();
|
||||||
|
|
||||||
return PyLong_FromLong((long)hook);
|
return PyLong_FromLong((long)hook);
|
||||||
@ -1825,7 +2037,43 @@ Module_hexchat_hook_print(PyObject *self, PyObject *args, PyObject *kwargs)
|
|||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
BEGIN_XCHAT_CALLS(NONE);
|
BEGIN_XCHAT_CALLS(NONE);
|
||||||
hook->data = (void*)hexchat_hook_print(ph, name, priority,
|
hook->data = (void*)hexchat_hook_print_attrs(ph, name, priority,
|
||||||
|
Callback_Print, hook);
|
||||||
|
END_XCHAT_CALLS();
|
||||||
|
|
||||||
|
return PyLong_FromLong((long)hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
Module_hexchat_hook_print_attrs(PyObject *self, PyObject *args, PyObject *kwargs)
|
||||||
|
{
|
||||||
|
char *name;
|
||||||
|
PyObject *callback;
|
||||||
|
PyObject *userdata = Py_None;
|
||||||
|
int priority = HEXCHAT_PRI_NORM;
|
||||||
|
PyObject *plugin;
|
||||||
|
Hook *hook;
|
||||||
|
char *kwlist[] = {"name", "callback", "userdata", "priority", 0};
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|Oi:hook_print_attrs",
|
||||||
|
kwlist, &name, &callback, &userdata,
|
||||||
|
&priority))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
plugin = Plugin_GetCurrent();
|
||||||
|
if (plugin == NULL)
|
||||||
|
return NULL;
|
||||||
|
if (!PyCallable_Check(callback)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "callback is not callable");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
hook = Plugin_AddHook(HOOK_XCHAT_ATTR, plugin, callback, userdata, name, NULL);
|
||||||
|
if (hook == NULL)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
BEGIN_XCHAT_CALLS(NONE);
|
||||||
|
hook->data = (void*)hexchat_hook_print_attrs(ph, name, priority,
|
||||||
Callback_Print, hook);
|
Callback_Print, hook);
|
||||||
END_XCHAT_CALLS();
|
END_XCHAT_CALLS();
|
||||||
|
|
||||||
@ -2068,6 +2316,8 @@ static PyMethodDef Module_xchat_methods[] = {
|
|||||||
METH_VARARGS},
|
METH_VARARGS},
|
||||||
{"emit_print", Module_hexchat_emit_print,
|
{"emit_print", Module_hexchat_emit_print,
|
||||||
METH_VARARGS},
|
METH_VARARGS},
|
||||||
|
{"emit_print_at", Module_hexchat_emit_print_at,
|
||||||
|
METH_VARARGS},
|
||||||
{"get_info", Module_hexchat_get_info,
|
{"get_info", Module_hexchat_get_info,
|
||||||
METH_VARARGS},
|
METH_VARARGS},
|
||||||
{"get_prefs", Module_xchat_get_prefs,
|
{"get_prefs", Module_xchat_get_prefs,
|
||||||
@ -2088,8 +2338,12 @@ static PyMethodDef Module_xchat_methods[] = {
|
|||||||
METH_VARARGS|METH_KEYWORDS},
|
METH_VARARGS|METH_KEYWORDS},
|
||||||
{"hook_server", (PyCFunction)Module_hexchat_hook_server,
|
{"hook_server", (PyCFunction)Module_hexchat_hook_server,
|
||||||
METH_VARARGS|METH_KEYWORDS},
|
METH_VARARGS|METH_KEYWORDS},
|
||||||
|
{"hook_server_attrs", (PyCFunction)Module_hexchat_hook_server_attrs,
|
||||||
|
METH_VARARGS|METH_KEYWORDS},
|
||||||
{"hook_print", (PyCFunction)Module_hexchat_hook_print,
|
{"hook_print", (PyCFunction)Module_hexchat_hook_print,
|
||||||
METH_VARARGS|METH_KEYWORDS},
|
METH_VARARGS|METH_KEYWORDS},
|
||||||
|
{"hook_print_attrs", (PyCFunction)Module_hexchat_hook_print_attrs,
|
||||||
|
METH_VARARGS|METH_KEYWORDS},
|
||||||
{"hook_timer", (PyCFunction)Module_hexchat_hook_timer,
|
{"hook_timer", (PyCFunction)Module_hexchat_hook_timer,
|
||||||
METH_VARARGS|METH_KEYWORDS},
|
METH_VARARGS|METH_KEYWORDS},
|
||||||
{"hook_unload", (PyCFunction)Module_hexchat_hook_unload,
|
{"hook_unload", (PyCFunction)Module_hexchat_hook_unload,
|
||||||
|
Loading…
Reference in New Issue
Block a user