From a602590e54a30dca0982c43411d1abefeb606a4b Mon Sep 17 00:00:00 2001 From: Geo Date: Sun, 12 Feb 2023 19:47:13 -0500 Subject: [PATCH] Update woobie mod docs --- doc/html/index.html | 24 +- doc/html/modules/internals.html | 396 ++++++++++++++++++++++++ doc/html/tutorials/module.html | 373 ++++++++++++++++++++++ doc/sphinx_source/index.rst | 2 + doc/sphinx_source/modules/internals.rst | 264 ++++++++++++++++ doc/sphinx_source/modules/writing.rst | 6 +- doc/sphinx_source/tutorials/module.rst | 235 ++++++++++++++ 7 files changed, 1297 insertions(+), 3 deletions(-) create mode 100644 doc/html/modules/internals.html create mode 100644 doc/html/tutorials/module.html create mode 100644 doc/sphinx_source/modules/internals.rst create mode 100644 doc/sphinx_source/tutorials/module.rst diff --git a/doc/html/index.html b/doc/html/index.html index 412af9939..31d1e7456 100644 --- a/doc/html/index.html +++ b/doc/html/index.html @@ -68,12 +68,14 @@

Table of Contents

  • Common First Steps
  • Enabling TLS Security on Eggdrop
  • Writing an Eggdrop Script
  • +
  • Writing an Eggdrop Module
  • Eggdrop Modules

    About Eggdrop

    @@ -339,6 +350,17 @@

    Where to find more helpWhat to do with a module? +
  • Eggdrop Bind Internals +
  • @@ -375,7 +397,7 @@

    Where to find more help © Copyright 2022, Eggheads. - Last updated on Dec 12, 2022. + Last updated on Jan 08, 2023. Created using Sphinx 1.8.5.

    diff --git a/doc/html/modules/internals.html b/doc/html/modules/internals.html new file mode 100644 index 000000000..9faf08e66 --- /dev/null +++ b/doc/html/modules/internals.html @@ -0,0 +1,396 @@ + + + + + + + + Eggdrop Bind Internals — Eggdrop 1.9.4 documentation + + + + + + + + + + + + + +
    +
    + +
    + +
    +
    +
    + +
    +

    Eggdrop Bind Internals

    +

    This document is intended for C developers who want to understand how Eggdrop’s Tcl binds or C binds work.

    +

    For documentation purposes the “dcc” bind type is used as an example.

    +

    It already exists and is suitable to illustrate the details of bind handling in Eggdrop.

    +

    Note: All code snippets are altered for brevity and simplicity, see original source code for the full and current versions.

    +
    +

    Bind Table Creation

    +

    The bind table is added by calling, either at module initialization or startup:

    +
    /* Global symbol, available to other C files with
    + * extern p_tcl_bind_list H_dcc;
    + */
    +p_tcl_bind_list H_dcc;
    +
    +/* Creating the bind table:
    + * @param[in] const char *name Limited in length, see tclhash.h
    + * @param[in] int flags        HT_STACKABLE or 0
    + * @param[in] IntFunc          Function pointer to C handler
    + * @return    p_tcl_bind_list  aka (tcl_bind_list_t *)
    + */
    +H_dcc = add_bind_table("dcc", 0, builtin_dcc);
    +
    +
    +

    What the C handler does is explained later, because a lot happens before it is actually called. IntFunc is a generic function pointer that returns an int with arbitrary arguments.

    +

    H_dcc can be exported from core and imported into modules as any other variable or function. That should be explained in a separate document.

    +
    +
    +

    Stackable Binds: HT_STACKABLE

    +

    HT_STACKABLE means that multiple binds can exist for the same mask.

    +
    bind dcc - test proc1; # not stackable
    +bind dcc - test proc2; # overwrites the first one, only proc2 will be called
    +
    +
    +

    It does not automatically call multiple binds that match, see later in the Triggering any Bind section for details.

    +
    +
    +

    Tcl Binding

    +

    After the bind table is created with add_bind_table, Tcl procs can already be registered to this bind by calling:

    +
    bind dcc -|- test myproc
    +proc myproc {args} {
    +  putlog "myproc was called, argument list: '[join $args ',']'"
    +  return 0
    +}
    +
    +
    +

    Of course it is not clear so far:

    +
      +
    • If flags -|- matter for this bind at all and what they are checked against
    • +
    • If channel flags have a meaning or global/bot only
    • +
    • What test is matched against to see if the bind should trigger
    • +
    • Which arguments myproc receives, the example just accepts all arguments
    • +
    +
    +
    +

    Triggering the Bind

    +

    To trigger the bind and call it with the desired arguments, a function is created.

    +
    int check_tcl_dcc(const char *cmd, int idx, const char *args) {
    +  struct flag_record fr = { FR_GLOBAL | FR_CHAN, 0, 0, 0, 0, 0 };
    +  int x;
    +  char s[11];
    +
    +  get_user_flagrec(dcc[idx].user, &fr, dcc[idx].u.chat->con_chan);
    +  egg_snprintf(s, sizeof s, "%ld", dcc[idx].sock);
    +  Tcl_SetVar(interp, "_dcc1", (char *) dcc[idx].nick, 0);
    +  Tcl_SetVar(interp, "_dcc2", (char *) s, 0);
    +  Tcl_SetVar(interp, "_dcc3", (char *) args, 0);
    +  x = check_tcl_bind(H_dcc, cmd, &fr, " $_dcc1 $_dcc2 $_dcc3",
    +                  MATCH_PARTIAL | BIND_USE_ATTR | BIND_HAS_BUILTINS);
    +  /* snip ..., return code handling */
    +  return 0;
    +}
    +
    +
    +

    The global Tcl variables $_dcc1 $_dcc2 $_dcc3 are used as temporary string variables and passed as arguments to the registered Tcl proc.

    +

    This shows which arguments the callbacks in Tcl get:

    +
      +
    • the nickname of the DCC chat user (handle of the user)
    • +
    • the IDX (socket id) of the partyline so [putdcc] can respond back
    • +
    • another string argument that depends on the caller
    • +
    +

    The call to check_tcl_dcc can be found in the DCC parsing in src/dcc.c.

    +
    +
    +

    Triggering any Bind

    +

    check_tcl_bind is used by all binds and does the following:

    +
    /* Generic function to call one/all matching binds
    + * @param[in] tcl_bind_list_t *tl      Bind table (e.g. H_dcc)
    + * @param[in] const char *match        String to match the bind-masks against
    + * @param[in] struct flag_record *atr  Flags of the user calling the bind
    + * @param[in] const char *param        Arguments to add to the bind callback proc (e.g. " $_dcc1 $_dcc2 $_dcc3")
    + * @param[in] int match_type           Matchtype and various flags
    + * @returns   int                      Match result code
    + */
    +
    +/* Source code changed, only illustrative */
    +int check_tcl_bind(tcl_bind_list_t *tl, const char *match, struct flag_record *atr, const char *param, int match_type) {
    +  int x = BIND_NOMATCH;
    +  for (tm = tl->first; tm && !finish; tm_last = tm, tm = tm->next) {
    +    /* Check if bind mask matches */
    +    if (!check_bind_match(match, tm->mask, match_type))
    +      continue;
    +    for (tc = tm->first; tc; tc = tc->next) {
    +      /* Check if the provided flags suffice for this command. */
    +      if (check_bind_flags(&tc->flags, atr, match_type)) {
    +        tc->hits++;
    +        /* not much more than Tcl_Eval(interp, "<procname> <arguments>"); and grab the result */
    +        x = trigger_bind(tc->func_name, param, tm->mask);
    +      }
    +    }
    +  }
    +  return x;
    +}
    +
    +
    +

    The supplied flags to check_tcl_bind in check_tcl_dcc are what defines how matching is performed.

    +

    In the case of a DCC bind we had:

    +
      +
    • Matchtype MATCH_PARTIAL: Prefix-Matching if the command can be uniquely identified (e.g. dcc .help calls .help)
    • +
    • Additional flag BIND_USE_ATTR: Flags are checked
    • +
    • Additional flag BIND_HAS_BUILTINS: Something with flag matching, unsure
    • +
    +

    For details on the available match types (wildcard matching, exact matching, etc.) see src/tclegg.h. Additional flags are also described there as well as the return codes of check_tcl_bind (e.g. BIND_NOMATCH).

    +

    Note: For a bind type to be stackable it needs to be registered with HT_STACKABLE AND check_tcl_bind must be called with BIND_STACKABLE.

    +
    +
    +

    C Binding

    +

    To create a C function that is called by the bind, Eggdrop provides the add_builtins function.

    +
    /* Add a list of C function callbacks to a bind
    + * @param[in] tcl_bind_list_t *  the bind type (e.g. H_dcc)
    + * @param[in] cmd_t *            a NULL-terminated table of binds:
    + * cmd_t *mycmds = {
    + *   {char *name, char *flags, IntFunc function, char *tcl_name},
    + *   ...,
    + *   {NULL, NULL, NULL, NULL}
    + * };
    + */
    +void add_builtins(tcl_bind_list_t *tl, cmd_t *cc) {
    +  char p[1024];
    +  cd_tcl_cmd tclcmd;
    +
    +  tclcmd.name = p;
    +  tclcmd.callback = tl->func;
    +  for (i = 0; cc[i].name; i++) {
    +    /* Create Tcl command with automatic or given names *<bindtype>:<funcname>, e.g.
    +     * - H_raw {"324", "", got324, "irc:324"} => *raw:irc:324
    +     * - H_dcc {"boot", "t", cmd_boot, NULL} => *dcc:boot
    +     */
    +    egg_snprintf(p, sizeof p, "*%s:%s", tl->name, cc[i].funcname ? cc[i].funcname : cc[i].name);
    +    /* arbitrary void * can be included, we include C function pointer */
    +    tclcmd.cdata = (void *) cc[i].func;
    +    add_cd_tcl_cmd(tclcmd);
    +    bind_bind_entry(tl, cc[i].flags, cc[i].name, p);
    +  }
    +}
    +
    +
    +

    It automatically creates Tcl commands (e.g. *dcc:cmd_boot) that will call the C handler from add_bind_table in the first section Bind Table Creation and it gets a context (void *) argument with the C function it is supposed to call (e.g. cmd_boot()).

    +

    Now we can actually look at the C function handler for dcc as an example and what it has to implement.

    +
    +
    +

    C Handler

    +

    The example handler for DCC looks as follows:

    +
    /* Typical Tcl_Command arguments, just like e.g. tcl_putdcc is a Tcl/C command for [putdcc] */
    +static int builtin_dcc (ClientData cd, Tcl_Interp *irp, int argc, char *argv[]) {
    +  int idx;
    +  /* F: The C function we want to call, if the bind is okay, e.g. cmd_boot() */
    +  Function F = (Function) cd;
    +
    +  /* Task of C function: verify argument count and syntax as any Tcl command */
    +  BADARGS(4, 4, " hand idx param");
    +
    +  /* C Macro only used in C handlers for bind types, sanity checks the Tcl proc name
    +   * for *<bindtype>:<name> and that we are in the right C handler
    +   */
    +  CHECKVALIDITY(builtin_dcc);
    +
    +  idx = findidx(atoi(argv[2]));
    +  if (idx < 0) {
    +      Tcl_AppendResult(irp, "invalid idx", NULL);
    +      return TCL_ERROR;
    +  }
    +
    +  /* Call the desired C function, e.g. cmd_boot() with their arguments */
    +  F(dcc[idx].user, idx, argv[3]);
    +  Tcl_ResetResult(irp);
    +  Tcl_AppendResult(irp, "0", NULL);
    +  return TCL_OK;
    +}
    +
    +
    +

    This is finally the part where we see the arguments a C function gets for a DCC bind as opposed to a Tcl proc.

    +

    F(dcc[idx].user, idx, argv[3]):

    +
      +
    • User information as struct userrec *
    • +
    • IDX as int
    • +
    • The 3rd string argument from the Tcl call to *dcc:cmd_boot, which was $_dcc3 which was args to check_tcl_dcc which was everything after the dcc command
    • +
    +

    So this is how we register C callbacks for binds with the correct arguments:

    +
    /* We know the return value is ignored because the return value of F
    + * in builtin_dcc is ignored, so it can be void, but for other binds
    + * it could be something else and used in the C handler for the bind.
    + */
    +void cmd_boot(struct userrec *u, int idx, char *par) { /* snip */ }
    +
    +cmd_t *mycmds = {
    +  {"boot", "t", (IntFunc) cmd_boot, NULL /* automatic name: *dcc:boot */},
    +  {NULL, NULL, NULL, NULL}
    +};
    +add_builtins(H_dcc, mycmds);
    +
    +
    +
    +
    +

    Summary

    +

    In summary, this is how the dcc bind is called:

    +
      +
    • check_tcl_dcc() creates Tcl variables $_dcc1 $_dcc2 $_dcc3 and lets check_tcl_bind call the binds
    • +
    • Tcl binds are done at this point
    • +
    • C binds mean the Tcl command associated with the bind is *dcc:boot which calls builtin_dcc which gets cmd_boot as ClientData cd argument
    • +
    • buildin_dcc performs some sanity checking to avoid crashes and then calls cmd_boot() aka F() with the arguments it wants C callbacks to have
    • +
    +

    Example edited and annotated gdb backtrace in cmd_boot after doing .boot test on the partyline as user thommey with typical owner flags.

    +
    #0  cmd_boot (u=0x55e8bd8a49b0, idx=4, par=0x55e8be6a0010 "test") at cmds.c:614
    +    *u = {next = 0x55e8bd8aec90, handle = "thommey", flags = 8977024, flags_udef = 0, chanrec = 0x55e8bd8aeae0, entries = 0x55e8bd8a4a10}
    +#1  builtin_dcc (cd=0x55e8bbf002d0 <cmd_boot>, irp=0x55e8bd59b1c0, argc=4, argv=0x55e8bd7e3e00) at tclhash.c:678
    +    idx = 4
    +    argv = {0x55e8be642fa0 "*dcc:boot", 0x55e8be9f6bd0 "thommey", 0x55e8be7d9020 "4", 0x55e8be6a0010 "test", 0x0}
    +    F = 0x55e8bbf002d0 <cmd_boot>
    +#5  Tcl_Eval (interp=0x55e8bd59b1c0, script = "*dcc:boot $_dcc1 $_dcc2 $_dcc3") from /usr/lib/x86_64-linux-gnu/libtcl8.6.so
    +    Tcl: return $_dcc1 = "thommey"
    +    Tcl: return $_dcc2 = "4"
    +    Tcl: return $_dcc3 = "test"
    +    Tcl: return $lastbind = "boot" (set automatically by trigger_bind)
    +#8  trigger_bind (proc=proc@entry=0x55e8bd5efda0 "*dcc:boot", param=param@entry=0x55e8bbf4112b " $_dcc1 $_dcc2 $_dcc3", mask=mask@entry=0x55e8bd5efd40 "boot") at tclhash.c:742
    +#9  check_tcl_bind (tl=0x55e8bd5eecb0 <H_dcc>, match=match@entry=0x7ffcf3f9dac1 "boot", atr=atr@entry=0x7ffcf3f9d100, param=param@entry=0x55e8bbf4112b " $_dcc1 $_dcc2 $_dcc3", match_type=match_type@entry=80) at tclhash.c:942
    +    proc = 0x55e8bd5efda0 "*dcc:boot"
    +    mask = 0x55e8bd5efd40 "boot"
    +    brkt = 0x7ffcf3f9dac6 "test"
    +#10 check_tcl_dcc (cmd=cmd@entry=0x7ffcf3f9dac1 "boot", idx=idx@entry=4, args=0x7ffcf3f9dac6 "test") at tclhash.c:974
    +    fr = {match = 5, global = 8977024, udef_global = 0, bot = 0, chan = 0, udef_chan = 0}
    +#11 dcc_chat (idx=idx@entry=4, buf=<optimized out>, i=<optimized out>) at dcc.c:1068
    +    v = 0x7ffcf3f9dac1 "boot"
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + \ No newline at end of file diff --git a/doc/html/tutorials/module.html b/doc/html/tutorials/module.html new file mode 100644 index 000000000..b750801ea --- /dev/null +++ b/doc/html/tutorials/module.html @@ -0,0 +1,373 @@ + + + + + + + + Writing an Eggdrop Module — Eggdrop 1.9.4 documentation + + + + + + + + + + + + + +
    +
    + +
    + +
    +
    +
    + +
    +

    Writing an Eggdrop Module

    +

    An Eggdrop module is a piece of C code that can be loaded (or unloaded) onto the core Eggdrop code. A module differs from a Tcl script in that modules must be compiled and then loaded, whereas scripts can be edited and loaded directly. Importantly, a module can be written to create new Eggdrop-specific Tcl commands and binds that a user can then use in a Tcl script. For example, the server module loaded by Eggdrop is what creates the “jump” Tcl command, which causes tells the Eggdrop to jump to the next server in its server list.

    +

    There are a few required functions a module must perform in order to properly work with Eggdrop

    +
    +

    Module Header

    +

    A module should include license information. This tells other open source users how they are allowed to use the code. Eggdrop uses GPL 2.0 licensing, and our license information looks like this:

    +
    /*
    +* 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 2
    +* 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, write to the Free Software
    +* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    +*/
    +
    +
    +
    +
    +

    Required Code

    +

    For this section, you don’t necessarily need to understand what it is doing, but this code is required for a module to function.

    +

    You’ll next want to name your module:

    +
    #define MODULE_NAME "woobie"
    +
    +
    +

    Declare your own function tables (you don’t need to understand this part; you just need to copy/paste it):

    +
    #undef global
    +static Function *global = NULL, *server_funcs = NULL;
    +EXPORT_SCOPE char *woobie_start();
    +
    +
    +

    Next are two memory-related functions used by the core Eggdrop .status and .module commands:

    +
    static int woobie_expmem()
    +{
    +  int size = 0;
    +
    +  return size;
    +}
    +
    +static void woobie_report(int idx, int details)
    +{
    +  if (details) {
    +    int size = woobie_expmem();
    +
    +    dprintf(idx, "    Using %d byte%s of memory\n", size,
    +            (size != 1) ? "s" : "");
    +  }
    +}
    +
    +
    +

    This function is called when Eggdrop loads the module:

    +
    char *woobie_start(Function *global_funcs)
    +{
    +  global = global_funcs;
    +
    +  /* Register the module. */
    +  module_register(MODULE_NAME, woobie_table, 2, 1);
    +  /*                                            ^--- minor module version
    +   *                                         ^------ major module version
    +   *                           ^-------------------- module function table
    +   *              ^--------------------------------- module name
    +   */
    +
    +  if (!module_depend(MODULE_NAME, "eggdrop", 108, 0)) {
    +    module_undepend(MODULE_NAME);
    +    return "This module requires Eggdrop 1.8.0 or later.";
    +  }
    +
    +
    +

    This next function is used to unload the module:

    +
    static char *woobie_close()
    +{
    +  module_undepend(MODULE_NAME);
    +  return NULL;
    +}
    +
    +
    +

    This creates a function table that is exported to Eggdrop. In other words, these are commands that are made available to the Eggdrop core and other modules. At minimum, the following functions must be exported:

    +
    static Function woobie_table[] = {
    +  (Function) woobie_start,
    +  (Function) woobie_close,
    +  (Function) woobie_expmem,
    +  (Function) woobie_report,
    +};
    +
    +
    +

    At this point, you should have a module that compiles and can be loaded by Eggdrop- but dosen’t really do anything yet. We’ll change that in the next section!

    +
    +
    +

    Adding a Partyline Command

    +

    A partyline command function accepts three arguments- a pointer to the user record of the user that called the command; the idx the user was on when calling the command; and a pointer to the arguments appended to the command. A command should immediately log that it was called to the LOG_CMDS log level, and then run its desired code. This simple example prints “WOOBIE” to the partyline idx of the user that called it:

    +
    static int cmd_woobie(struct userrec *u, int idx, char *par)
    +{
    +  putlog(LOG_CMDS, "*", "#%s# woobie", dcc[idx].nick);
    +  dprintf(idx, "WOOBIE!\n");
    +  return 0;
    +}
    +
    +
    +

    If you add partyline commands, you need to create a table which links the new command name to the function it should call. This can be done like so:

    +
    static cmd_t mywoobie[] = {
    +  /* command  flags  function     tcl-name */
    +  {"woobie",  "",    cmd_woobie,  NULL},
    +  {NULL,      NULL,  NULL,        NULL}  /* Mark end. */
    +};
    +
    +
    +

    The tcl-name field can be a name for a Tcl command that will also call the partyline command, or it can be left as NULL.

    +
    +
    +

    Adding a Tcl Command

    +

    Eggdrop uses the Tcl C API library to interact with the Tcl interpreter. Learning this API is outside the scope of this tutorial, but this example Tcl command will echo the provided argument:

    +
    static int tcl_echome STDVAR {
    +  BADARGS(2, 2, " arg");
    +
    +  if (strcmp(argv[1], "llama") {
    +    Tcl_AppendResult(irp, "You said: ", argv[1], NULL);
    +    return TCL_OK;
    +  } else {
    +    Tcl_AppendResult(irp, "illegal word!");
    +    return TCL_ERROR;
    +  }
    +}
    +
    +A few notes on this example. BADARGS is a macro that checks the input provided to the Tcl command. The first argument BADARGS accepts is the minimum number of paramters the Tcl command must accept (including the command itself). The second argument is the maximum number of parameters that BADARGS will accept. The third argument is the help text that will be displayed if these boundaries are exceeded. For example, BADARGS(2, 4, " name ?date? ?place?") requires at least one argument to be passed, and a maximum of three arguments. Eggdrop code style is to enclose optional arguments between qusetion marks in the help text.
    +
    +
    +

    Similar to adding a partyline command, you also have to create a function table for a new Tcl command:

    +
    static tcl_cmds mytcl[] = {
    +  {"echome",           tcl_echome},
    +  {NULL,                   NULL}   /* Required to mark end of table */
    +};
    +
    +
    +

    And now the newly-created Tcl command ‘echome’ is available for use in a script!

    +
    +
    +

    Adding a Tcl Bind

    +

    A Tcl bind is a command that is activated when a certain condition is met. With Eggdrop, these are usually linked to receiving messages or other IRC events. To create a bind, you must first register the bind type with Eggdrop when the module is loaded (you added the woobie_start() and woobie_close functions earlier, you still need all that earlier code in here as well):

    +
    static p_tcl_bind_list H_woob;
    +
    +...
    +
    +char *woobie_start(Function *global_funcs)
    +{
    +  ...
    +  H_woob = add_bind_table("woobie", HT_STACKABLE, woobie_2char);
    +}
    +
    +
    +

    And then remove the binds when the module is unloaded:

    +
    static char *woobie_close()
    +{
    +  ...
    +  del_bind_table(H_woob);
    +}
    +
    +
    +

    Here, “woobie” is the name of the bind (similar to the PUB, MSG, JOIN types of binds you already see in tcl-commands.doc). HT_STACKABLE means you can have multiple binds of this type. “woobie_2char” defines how many arguments the bind will take, and we’ll talk about that next.

    +
    +

    Defining bind arguments

    +

    The following code example defines a bind that will take two arguments:

    +
    static int woobie_2char STDVAR
    +{
    +  Function F = (Function) cd;
    +
    +  BADARGS(3, 3, " nick chan");
    +
    +  CHECKVALIDITY(woobie_2char);
    +  F(argv[1], argv[2]);
    +  return TCL_OK;
    +}
    +
    +
    +

    And this example defines a bind that will take three arguments:

    +
    static int woobie_3char STDVAR
    +{
    +  Function F = (Function) cd;
    +
    +  BADARGS(4, 4, " foo bar moo");
    +
    +  CHECKVALIDITY(woobie_3char);
    +  F(argv[1], argv[2], argv[3]);
    +  return TCL_OK;
    +}
    +
    +
    +

    Like before, BADARGS still checks that the number of arguments passed is correct, and outputs help text if it is not. The rest is boilerplate code to pass the arguments when the bind is called.

    +
    +
    +

    Calling the Bind

    +

    To call the bind, Eggdrop coding style it to name that function “check_tcl_bindname”. So here, whenever we reach a point in code that should trigger the bind, we’ll call check_tcl_woobie() and pass the arguments we defined- in this case, two arguments that woobie_2char was created to handle. Here is some sample code:

    +
    check_tcl_woobie(chan, nick);
    +
    +
    +static int check_tcl_woobie(char *chan, char *nick, char *userhost) {
    +  int x;
    +  char mask[1024];
    +  struct flag_record fr = { FR_GLOBAL | FR_CHAN, 0, 0, 0, 0, 0 };
    +
    +  snprintf(mask, sizeof mask, "%s %s!%s",
    +                                chan, nick, userhost);
    +  Tcl_SetVar(interp, "_woob1", nick ? (char *) nick : "", 0);
    +  Tcl_SetVar(interp, "_woob2", chan, 0);
    +  x = check_tcl_bind(H_woob, mask, &fr, " $_woob1 $_woob2",
    +        MATCH_MASK | BIND_STACKABLE);
    +  return (x == BIND_EXEC_LOG);
    +}
    +
    +
    +

    Now that we have encountered a condition that triggers the bind, we need to check it against the binds the user has loaded in scripts and see if it matches those conditions. This is done with check_tcl_bind(), called with the bind type, the userhost of the user, the flag record of the user if it exists, the bind arguments, and bind options.

    +
    +
    +
    +

    Exporting the Bind

    +

    Do we need to do this?

    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + + + + + \ No newline at end of file diff --git a/doc/sphinx_source/index.rst b/doc/sphinx_source/index.rst index dca64902a..5e0178241 100644 --- a/doc/sphinx_source/index.rst +++ b/doc/sphinx_source/index.rst @@ -78,6 +78,7 @@ The Eggheads development team can be found lurking on #eggdrop on the Libera net tutorials/firststeps tutorials/tlssetup tutorials/firstscript + tutorials/module.rst .. toctree:: :caption: Eggdrop Modules @@ -86,6 +87,7 @@ The Eggheads development team can be found lurking on #eggdrop on the Libera net modules/index modules/included modules/writing + modules/internals.rst .. toctree:: :caption: About Eggdrop diff --git a/doc/sphinx_source/modules/internals.rst b/doc/sphinx_source/modules/internals.rst new file mode 100644 index 000000000..5e026a391 --- /dev/null +++ b/doc/sphinx_source/modules/internals.rst @@ -0,0 +1,264 @@ +Eggdrop Bind Internals +====================== + +This document is intended for C developers who want to understand how Eggdrop’s Tcl binds or C binds work. + +For documentation purposes the “dcc” bind type is used as an example. + +It already exists and is suitable to illustrate the details of bind handling in Eggdrop. + +Note: All code snippets are altered for brevity and simplicity, see original source code for the full and current versions. + +Bind Table Creation +------------------- + +The bind table is added by calling, either at module initialization or startup:: + + /* Global symbol, available to other C files with + * extern p_tcl_bind_list H_dcc; + */ + p_tcl_bind_list H_dcc; + + /* Creating the bind table: + * @param[in] const char *name Limited in length, see tclhash.h + * @param[in] int flags HT_STACKABLE or 0 + * @param[in] IntFunc Function pointer to C handler + * @return p_tcl_bind_list aka (tcl_bind_list_t *) + */ + H_dcc = add_bind_table("dcc", 0, builtin_dcc); + +What the :code:`C handler` does is explained later, because a lot happens before it is actually called. :code:`IntFunc` is a generic function pointer that returns an :code:`int` with arbitrary arguments. + +:code:`H_dcc` can be exported from core and imported into modules as any other variable or function. That should be explained in a separate document. + +Stackable Binds: HT_STACKABLE +----------------------------- + +:code:`HT_STACKABLE` means that multiple binds can exist for the same mask. +:: + + bind dcc - test proc1; # not stackable + bind dcc - test proc2; # overwrites the first one, only proc2 will be called + +It does not automatically call multiple binds that match, see later in the `Triggering any Bind`_ section for details. + +Tcl Binding +----------- + +After the bind table is created with :code:`add_bind_table`, Tcl procs can already be registered to this bind by calling:: + + bind dcc -|- test myproc + proc myproc {args} { + putlog "myproc was called, argument list: '[join $args ',']'" + return 0 + } + +Of course it is not clear so far: + +* If flags :code:`-|-` matter for this bind at all and what they are checked against +* If channel flags have a meaning or global/bot only +* What :code:`test` is matched against to see if the bind should trigger +* Which arguments :code:`myproc` receives, the example just accepts all arguments + +Triggering the Bind +------------------- + +To trigger the bind and call it with the desired arguments, a function is created. +:: + + int check_tcl_dcc(const char *cmd, int idx, const char *args) { + struct flag_record fr = { FR_GLOBAL | FR_CHAN, 0, 0, 0, 0, 0 }; + int x; + char s[11]; + + get_user_flagrec(dcc[idx].user, &fr, dcc[idx].u.chat->con_chan); + egg_snprintf(s, sizeof s, "%ld", dcc[idx].sock); + Tcl_SetVar(interp, "_dcc1", (char *) dcc[idx].nick, 0); + Tcl_SetVar(interp, "_dcc2", (char *) s, 0); + Tcl_SetVar(interp, "_dcc3", (char *) args, 0); + x = check_tcl_bind(H_dcc, cmd, &fr, " $_dcc1 $_dcc2 $_dcc3", + MATCH_PARTIAL | BIND_USE_ATTR | BIND_HAS_BUILTINS); + /* snip ..., return code handling */ + return 0; + } + +The global Tcl variables :code:`$_dcc1 $_dcc2 $_dcc3` are used as temporary string variables and passed as arguments to the registered Tcl proc. + +This shows which arguments the callbacks in Tcl get: + +* the nickname of the DCC chat user (handle of the user) +* the IDX (socket id) of the partyline so :code:`[putdcc]` can respond back +* another string argument that depends on the caller + +The call to :code:`check_tcl_dcc` can be found in the DCC parsing in `src/dcc.c`. + +Triggering any Bind +------------------- + +`check_tcl_bind` is used by all binds and does the following:: + + /* Generic function to call one/all matching binds + * @param[in] tcl_bind_list_t *tl Bind table (e.g. H_dcc) + * @param[in] const char *match String to match the bind-masks against + * @param[in] struct flag_record *atr Flags of the user calling the bind + * @param[in] const char *param Arguments to add to the bind callback proc (e.g. " $_dcc1 $_dcc2 $_dcc3") + * @param[in] int match_type Matchtype and various flags + * @returns int Match result code + */ + + /* Source code changed, only illustrative */ + int check_tcl_bind(tcl_bind_list_t *tl, const char *match, struct flag_record *atr, const char *param, int match_type) { + int x = BIND_NOMATCH; + for (tm = tl->first; tm && !finish; tm_last = tm, tm = tm->next) { + /* Check if bind mask matches */ + if (!check_bind_match(match, tm->mask, match_type)) + continue; + for (tc = tm->first; tc; tc = tc->next) { + /* Check if the provided flags suffice for this command. */ + if (check_bind_flags(&tc->flags, atr, match_type)) { + tc->hits++; + /* not much more than Tcl_Eval(interp, " "); and grab the result */ + x = trigger_bind(tc->func_name, param, tm->mask); + } + } + } + return x; + } + +The supplied flags to :code:`check_tcl_bind` in `check_tcl_dcc` are what defines how matching is performed. + +In the case of a DCC bind we had: + +* Matchtype :code:`MATCH_PARTIAL`: Prefix-Matching if the command can be uniquely identified (e.g. dcc .help calls .help) +* Additional flag :code:`BIND_USE_ATTR`: Flags are checked +* Additional flag :code:`BIND_HAS_BUILTINS`: Something with flag matching, unsure + +For details on the available match types (wildcard matching, exact matching, etc.) see :code:`src/tclegg.h`. Additional flags are also described there as well as the return codes of :code:`check_tcl_bind` (e.g. :code:`BIND_NOMATCH`). + +Note: For a bind type to be stackable it needs to be registered with :code:`HT_STACKABLE` AND :code:`check_tcl_bind` must be called with :code:`BIND_STACKABLE`. + +C Binding +--------- + +To create a C function that is called by the bind, Eggdrop provides the :code:`add_builtins` function. +:: + + /* Add a list of C function callbacks to a bind + * @param[in] tcl_bind_list_t * the bind type (e.g. H_dcc) + * @param[in] cmd_t * a NULL-terminated table of binds: + * cmd_t *mycmds = { + * {char *name, char *flags, IntFunc function, char *tcl_name}, + * ..., + * {NULL, NULL, NULL, NULL} + * }; + */ + void add_builtins(tcl_bind_list_t *tl, cmd_t *cc) { + char p[1024]; + cd_tcl_cmd tclcmd; + + tclcmd.name = p; + tclcmd.callback = tl->func; + for (i = 0; cc[i].name; i++) { + /* Create Tcl command with automatic or given names *:, e.g. + * - H_raw {"324", "", got324, "irc:324"} => *raw:irc:324 + * - H_dcc {"boot", "t", cmd_boot, NULL} => *dcc:boot + */ + egg_snprintf(p, sizeof p, "*%s:%s", tl->name, cc[i].funcname ? cc[i].funcname : cc[i].name); + /* arbitrary void * can be included, we include C function pointer */ + tclcmd.cdata = (void *) cc[i].func; + add_cd_tcl_cmd(tclcmd); + bind_bind_entry(tl, cc[i].flags, cc[i].name, p); + } + } + +It automatically creates Tcl commands (e.g. :code:`*dcc:cmd_boot`) that will call the `C handler` from `add_bind_table` in the first section `Bind Table Creation`_ and it gets a context (void \*) argument with the C function it is supposed to call (e.g. `cmd_boot()`). + +Now we can actually look at the C function handler for dcc as an example and what it has to implement. + +C Handler +--------- + +The example handler for DCC looks as follows:: + + /* Typical Tcl_Command arguments, just like e.g. tcl_putdcc is a Tcl/C command for [putdcc] */ + static int builtin_dcc (ClientData cd, Tcl_Interp *irp, int argc, char *argv[]) { + int idx; + /* F: The C function we want to call, if the bind is okay, e.g. cmd_boot() */ + Function F = (Function) cd; + + /* Task of C function: verify argument count and syntax as any Tcl command */ + BADARGS(4, 4, " hand idx param"); + + /* C Macro only used in C handlers for bind types, sanity checks the Tcl proc name + * for *: and that we are in the right C handler + */ + CHECKVALIDITY(builtin_dcc); + + idx = findidx(atoi(argv[2])); + if (idx < 0) { + Tcl_AppendResult(irp, "invalid idx", NULL); + return TCL_ERROR; + } + + /* Call the desired C function, e.g. cmd_boot() with their arguments */ + F(dcc[idx].user, idx, argv[3]); + Tcl_ResetResult(irp); + Tcl_AppendResult(irp, "0", NULL); + return TCL_OK; + } + +This is finally the part where we see the arguments a C function gets for a DCC bind as opposed to a Tcl proc. + +code:`F(dcc[idx].user, idx, argv[3])`: + +* User information as struct userrec * +* IDX as int +* The 3rd string argument from the Tcl call to \*dcc:cmd_boot, which was :code:`$_dcc3` which was :code:`args` to :code:`check_tcl_dcc` which was everything after the dcc command + +So this is how we register C callbacks for binds with the correct arguments:: + + /* We know the return value is ignored because the return value of F + * in builtin_dcc is ignored, so it can be void, but for other binds + * it could be something else and used in the C handler for the bind. + */ + void cmd_boot(struct userrec *u, int idx, char *par) { /* snip */ } + + cmd_t *mycmds = { + {"boot", "t", (IntFunc) cmd_boot, NULL /* automatic name: *dcc:boot */}, + {NULL, NULL, NULL, NULL} + }; + add_builtins(H_dcc, mycmds); + +Summary +------- + +In summary, this is how the dcc bind is called: + +* :code:`check_tcl_dcc()` creates Tcl variables :code:`$_dcc1 $_dcc2 $_dcc3` and lets :code:`check_tcl_bind` call the binds +* Tcl binds are done at this point +* C binds mean the Tcl command associated with the bind is :code:`*dcc:boot` which calls :code:`builtin_dcc` which gets :code:`cmd_boot` as ClientData cd argument +* :code:`gbuildin_dcc` performs some sanity checking to avoid crashes and then calls :code:`cmd_boot()` aka :code:`F()` with the arguments it wants C callbacks to have + +Example edited and annotated gdb backtrace in :code::`cmd_boot` after doing :code:`.boot test` on the partyline as user :code:`thommey` with typical owner flags. +:: + + #0 cmd_boot (u=0x55e8bd8a49b0, idx=4, par=0x55e8be6a0010 "test") at cmds.c:614 + *u = {next = 0x55e8bd8aec90, handle = "thommey", flags = 8977024, flags_udef = 0, chanrec = 0x55e8bd8aeae0, entries = 0x55e8bd8a4a10} + #1 builtin_dcc (cd=0x55e8bbf002d0 , irp=0x55e8bd59b1c0, argc=4, argv=0x55e8bd7e3e00) at tclhash.c:678 + idx = 4 + argv = {0x55e8be642fa0 "*dcc:boot", 0x55e8be9f6bd0 "thommey", 0x55e8be7d9020 "4", 0x55e8be6a0010 "test", 0x0} + F = 0x55e8bbf002d0 + #5 Tcl_Eval (interp=0x55e8bd59b1c0, script = "*dcc:boot $_dcc1 $_dcc2 $_dcc3") from /usr/lib/x86_64-linux-gnu/libtcl8.6.so + Tcl: return $_dcc1 = "thommey" + Tcl: return $_dcc2 = "4" + Tcl: return $_dcc3 = "test" + Tcl: return $lastbind = "boot" (set automatically by trigger_bind) + #8 trigger_bind (proc=proc@entry=0x55e8bd5efda0 "*dcc:boot", param=param@entry=0x55e8bbf4112b " $_dcc1 $_dcc2 $_dcc3", mask=mask@entry=0x55e8bd5efd40 "boot") at tclhash.c:742 + #9 check_tcl_bind (tl=0x55e8bd5eecb0 , match=match@entry=0x7ffcf3f9dac1 "boot", atr=atr@entry=0x7ffcf3f9d100, param=param@entry=0x55e8bbf4112b " $_dcc1 $_dcc2 $_dcc3", match_type=match_type@entry=80) at tclhash.c:942 + proc = 0x55e8bd5efda0 "*dcc:boot" + mask = 0x55e8bd5efd40 "boot" + brkt = 0x7ffcf3f9dac6 "test" + #10 check_tcl_dcc (cmd=cmd@entry=0x7ffcf3f9dac1 "boot", idx=idx@entry=4, args=0x7ffcf3f9dac6 "test") at tclhash.c:974 + fr = {match = 5, global = 8977024, udef_global = 0, bot = 0, chan = 0, udef_chan = 0} + #11 dcc_chat (idx=idx@entry=4, buf=, i=) at dcc.c:1068 + v = 0x7ffcf3f9dac1 "boot" diff --git a/doc/sphinx_source/modules/writing.rst b/doc/sphinx_source/modules/writing.rst index da222fbe5..ceeabb4fe 100644 --- a/doc/sphinx_source/modules/writing.rst +++ b/doc/sphinx_source/modules/writing.rst @@ -1,5 +1,7 @@ -Writing an Eggdrop Module -========================= +.. _writing_module: + +How to Write an Eggdrop Module +============================== Note: This is for a simple module of 1 source file. diff --git a/doc/sphinx_source/tutorials/module.rst b/doc/sphinx_source/tutorials/module.rst new file mode 100644 index 000000000..b4743484f --- /dev/null +++ b/doc/sphinx_source/tutorials/module.rst @@ -0,0 +1,235 @@ +Writing a Basic Eggdrop Module +============================== + +An Eggdrop module is a piece of C code that can be loaded (or unloaded) onto the core Eggdrop code. A module differs from a Tcl script in that modules must be compiled and then loaded, whereas scripts can be edited and loaded directly. Importantly, a module can be written to create new Eggdrop-specific Tcl commands and binds that a user can then use in a Tcl script. For example, the server module loaded by Eggdrop is what creates the "jump" Tcl command, which causes tells the Eggdrop to jump to the next server in its server list. + +There are a few required functions a module must perform in order to properly work with Eggdrop + +Module Header +------------- + +A module should include license information. This tells other open source users how they are allowed to use the code. Eggdrop uses GPL 2.0 licensing, and our license information looks like this:: + + /* + * 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 2 + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +Required Code +------------- + +For this section, you don't necessarily need to understand what it is doing, but this code is required for a module to function. If you want to learn more about this, check out :ref:`writing_module` + +You'll next want to name your module:: + + #define MODULE_NAME "woobie" + +Declare your own function tables (again, you don't need to understand this part; you just need to copy/paste it):: + + #undef global + static Function *global = NULL, *server_funcs = NULL; + EXPORT_SCOPE char *woobie_start(); + +Next are two memory-related functions used by the core Eggdrop .status and .module commands:: + + static int woobie_expmem() + { + int size = 0; + + return size; + } + + static void woobie_report(int idx, int details) + { + if (details) { + int size = woobie_expmem(); + + dprintf(idx, " Using %d byte%s of memory\n", size, + (size != 1) ? "s" : ""); + } + } + +This function is called when Eggdrop loads the module:: + + char *woobie_start(Function *global_funcs) + { + global = global_funcs; + + /* Register the module. */ + module_register(MODULE_NAME, woobie_table, 2, 1); + /* ^--- minor module version + * ^------ major module version + * ^-------------------- module function table + * ^--------------------------------- module name + */ + + if (!module_depend(MODULE_NAME, "eggdrop", 108, 0)) { + module_undepend(MODULE_NAME); + return "This module requires Eggdrop 1.8.0 or later."; + } + +This next function is used to unload the module:: + + static char *woobie_close() + { + module_undepend(MODULE_NAME); + return NULL; + } + +This creates a function table that is exported to Eggdrop. In other words, these are commands that are made available to the Eggdrop core and other modules. At minimum, the following functions must be exported:: + + static Function woobie_table[] = { + (Function) woobie_start, + (Function) woobie_close, + (Function) woobie_expmem, + (Function) woobie_report, + }; + +At this point, you should have a module that compiles and can be loaded by Eggdrop- but dosen't really do anything yet. We'll change that in the next section! + +Adding a Partyline Command +-------------------------- + +A partyline command function accepts three arguments- a pointer to the user record of the user that called the command; the idx the user was on when calling the command; and a pointer to the arguments appended to the command. A command should immediately log that it was called to the LOG_CMDS log level, and then run its desired code. This simple example prints "WOOBIE" to the partyline idx of the user that called it:: + + static int cmd_woobie(struct userrec *u, int idx, char *par) + { + putlog(LOG_CMDS, "*", "#%s# woobie", dcc[idx].nick); + dprintf(idx, "WOOBIE!\n"); + return 0; + } + +If you add partyline commands, you need to create a table which links the new command name to the function it should call. This can be done like so:: + + static cmd_t mywoobie[] = { + /* command flags function tcl-name */ + {"woobie", "", cmd_woobie, NULL}, + {NULL, NULL, NULL, NULL} /* Mark end. */ + }; + +The tcl-name field can be a name for a Tcl command that will also call the partyline command, or it can be left as NULL. + +Adding a Tcl Command +-------------------- + +Eggdrop uses the Tcl C API library to interact with the Tcl interpreter. Learning this API is outside the scope of this tutorial, but this example Tcl command will echo the provided argument:: + + + static int tcl_echome STDVAR { + BADARGS(2, 2, " arg"); + + if (strcmp(argv[1], "llama") { + Tcl_AppendResult(irp, "You said: ", argv[1], NULL); + return TCL_OK; + } else { + Tcl_AppendResult(irp, "illegal word!"); + return TCL_ERROR; + } + } + + A few notes on this example. BADARGS is a macro that checks the input provided to the Tcl command. The first argument BADARGS accepts is the minimum number of paramters the Tcl command must accept (including the command itself). The second argument is the maximum number of parameters that BADARGS will accept. The third argument is the help text that will be displayed if these boundaries are exceeded. For example, BADARGS(2, 4, " name ?date? ?place?") requires at least one argument to be passed, and a maximum of three arguments. Eggdrop code style is to enclose optional arguments between qusetion marks in the help text. + +Similar to adding a partyline command, you also have to create a function table for a new Tcl command:: + + static tcl_cmds mytcl[] = { + {"echome", tcl_echome}, + {NULL, NULL} /* Required to mark end of table */ + }; + +And now the newly-created Tcl command 'echome' is available for use in a script! + +Adding a Tcl Bind +----------------- + +A Tcl bind is a command that is activated when a certain condition is met. With Eggdrop, these are usually linked to receiving messages or other IRC events. To create a bind, you must first register the bind type with Eggdrop when the module is loaded (you added the woobie_start() and woobie_close functions earlier, you still need all that earlier code in here as well):: + + static p_tcl_bind_list H_woob; + + ... + + char *woobie_start(Function *global_funcs) + { + ... + H_woob = add_bind_table("woobie", HT_STACKABLE, woobie_2char); + } + +And then remove the binds when the module is unloaded:: + + static char *woobie_close() + { + ... + del_bind_table(H_woob); + } + +Here, "woobie" is the name of the bind (similar to the PUB, MSG, JOIN types of binds you already see in tcl-commands.doc). HT_STACKABLE means you can have multiple binds of this type. "woobie_2char" defines how many arguments the bind will take, and we'll talk about that next. + +Defining bind arguments +^^^^^^^^^^^^^^^^^^^^^^^ + +The following code example defines a bind that will take two arguments:: + + static int woobie_2char STDVAR + { + Function F = (Function) cd; + + BADARGS(3, 3, " nick chan"); + + CHECKVALIDITY(woobie_2char); + F(argv[1], argv[2]); + return TCL_OK; + } + +And this example defines a bind that will take three arguments:: + + static int woobie_3char STDVAR + { + Function F = (Function) cd; + + BADARGS(4, 4, " foo bar moo"); + + CHECKVALIDITY(woobie_3char); + F(argv[1], argv[2], argv[3]); + return TCL_OK; + } + +Like before, BADARGS still checks that the number of arguments passed is correct, and outputs help text if it is not. The rest is boilerplate code to pass the arguments when the bind is called. + +Calling the Bind +^^^^^^^^^^^^^^^^ + +To call the bind, Eggdrop coding style it to name that function "check_tcl_bindname". So here, whenever we reach a point in code that should trigger the bind, we'll call check_tcl_woobie() and pass the arguments we defined- in this case, two arguments that woobie_2char was created to handle. Here is some sample code:: + + check_tcl_woobie(chan, nick); + + + static int check_tcl_woobie(char *chan, char *nick, char *userhost) { + int x; + char mask[1024]; + struct flag_record fr = { FR_GLOBAL | FR_CHAN, 0, 0, 0, 0, 0 }; + + snprintf(mask, sizeof mask, "%s %s!%s", + chan, nick, userhost); + Tcl_SetVar(interp, "_woob1", nick ? (char *) nick : "", 0); + Tcl_SetVar(interp, "_woob2", chan, 0); + x = check_tcl_bind(H_woob, mask, &fr, " $_woob1 $_woob2", + MATCH_MASK | BIND_STACKABLE); + return (x == BIND_EXEC_LOG); + } + +Now that we have encountered a condition that triggers the bind, we need to check it against the binds the user has loaded in scripts and see if it matches those conditions. This is done with check_tcl_bind(), called with the bind type, the userhost of the user, the flag record of the user if it exists, the bind arguments, and bind options. + +Exporting the Bind +------------------ + +Do we need to do this?