Skip to content

Instantly share code, notes, and snippets.

@tazjin
Last active September 7, 2022 15:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tazjin/d86eed44f603d040730838606f970cb0 to your computer and use it in GitHub Desktop.
Save tazjin/d86eed44f603d040730838606f970cb0 to your computer and use it in GitHub Desktop.
Knotty - a featureless IRC bot

knotty

(knotty = Nix + bot + y, duh!)


Knotty is an IRC bot with functionality that is defined by its users. It uses the Nix package manager's language for all logic.

The basic idea is that all channels that knotty sits in have their own namespace in which data or functions can be defined, and each of those namespaces is further nested per user.

Basic features

Knotty can do all sorts of things, for example:

Simple expression evaluation:

<tazjin> knotty: 2 * 2
<knotty> 4
<tazjin> knotty: let a = 15; in a * 2
<knotty> 30
<tazjin> knotty: map (n: 2 * n) [ 1 2 ]
<knotty> [ 2 4 ]

Persistent definitions:

Users can define variables:

<tazjin> knotty: def a 15
<knotty> tazjin.a = 15
<tazjin> knotty: a * 2
<knotty> 30

These definitions can also be accessed by other users!

<someone> knotty: 2 * tazjin.a
<knotty> 30

Knotty supports Yants types:

<tazjin> knotty: def string a 15
<knotty> error: Expected type 'string', but value '15' is of type 'int'
<tazjin> knotty: def int a 15
<knotty> tazjin.a = 15

Functions

In addition to plain variables, users can also define functions:

<tazjin> knotty: def greeter (name: "Hello, ${name}")
<knotty> tazjin.greeter = λ :: any -> any
<tazjin> knotty: greeter "rms"
<knotty> Hello, rms

Function types from Yants are supported:

<tazjin> knotty: def greeter [ string string ] (name: "Hello, ${name}")
<knotty> greeter = λ :: string -> string
<tazjin> knotty: greeter 42
<knotty> error: Expected type 'string', but value '42' is of type 'int'

Of course, other users can also access defined functions:

<someone> knotty: tazjin.greeter "channel!"
<knotty> Hello, channel!

Types

Some IRC-specific type definitions already exist in knotty and are used several times, the most important one being:

struct "message" {
    nick = string;
    host = string;
    text = string;
}

Events

An IRC bot becomes a lot more useful when it can react to events that happen in a channel. Knotty supports this via a special type of function, called an event handler.

Each handler specifies the type of event it would like to be invoked for. The supported events are these:

enum "event" [
    "message"   # Any message matching a regular expression
    "highlight" # Any message containing a specific highlight word
    "timer"     # An event fired at specified times
]

Event handlers are defined using the defhandler function.

defhandler :: symbol -> event -> any -> function -> handler

The argument following the event type is the event-specific configuration. The specified function receives the event payload.

Configuration and payloads

event config payload
message Any regex matching the entire message The matching message
highlight List of highlight words The matched highlight word
timer string with crontab-syntax TBD

Examples:

Setting a timer:

<tazjin> knotty: def annoy [ any string ] (_: "It's this again!!")
<knotty> tazjin.annoy = λ :: any -> string
<tazjin> knotty: defhandler annoying "timer" "@hourly" annoy
<knotty> defined handler 'tazjin.annoying' for 'timer' events
# ... full hour is reached ...
<knotty> It's this again!

Reacting to messages:

<tazjin> knotty: def greet [ message string ] (msg: "Hello, ${msg.nick}!")
<knotty> tazjin.greet = λ :: message -> string
<tazjin> knotty: defhandler greet "message" "^([H|h]e(y|llo)).*$" greet
<knotty> defined handler 'tazjin.greet' for 'message' events
# ... someone joins the channel ...
<someone> Hey there everyone
<knotty> Hello, someone!

Reacting to highlights:

<tazjin> knotty: def incorrect (enum "incorrect" ["would of" "could of"])
<knotty> tazjin.incorrect = incorrect
<tazjin> knotty: def correct [ incorrect string ] (i: incorrect.match i { "would of": "You probably meant 'would have'!"; "could of": "You probably meant 'could have'!"; })
<knotty> tazjin.correct = λ :: incorrect -> string
<tazjin> knotty: defhandler argh "highlight" incorrect.values correct
<knotty> defined handler 'tazjin.argh' for 'highlight' events
# ... a while later
<someone> so i could of eaten the chicken nuggets instead
<knotty> You probably meant 'could have'!

Namespaces, permissions and quotas

Under the hood, knotty maintains what is effectively a big attribute set of all definitions. Handlers are somewhat special and are stored outside of this attribute set, but lets ignore that for a moment.

At the top-level knotty knows about the IRC networks it is connected to, followed by channels, followed by users:

{
    # First level is networks
    freenode = {
        # Second level is channels
        "#nixos-chat" = {
            # Third level is users
            tazjin = {
                foo = ...;
                bar = ...;
            };
        };
    };
}

Definitions can live at any level, but by default a definition created by a user lives in their personal, channel-specific namespace.

All expressions are evaluated in the contexts of the user invoking them (this also goes for handlers defined by that user).

For example, if this invocation takes place on Freenode's #nixos-chat

<tazjin> knotty: a * 2

it will be executed as

with freenode; with "#nixos-chat"; with tazjin; a * 2

which takes precedence into account.

Channel operators have the ability to define items in the channel namespace, but the syntax for this has not yet been decided. Network-level definitions must be added by the bot administrator.

Handlers defined at the user-level are subject to an hourly execution quota and invocations will silently be skipped if the quota is exceeded.

TODO

  • What's the event payload for timers? I'm thinking it might just be a static string saying fired! or something.
  • Could timers support extended crontab syntax (e.g. @hourly)? Lets assume yes.
  • Should highlights deliver the whole message? Multiple arguments?
  • Define what actions look like (+ action -> event cycle), which actions are supported?
  • Functions intentionally can not modify state, but maybe actions can be used for that? (imagine implementing a relay between channels just using knotty)
  • Is there a reliable way to use channel permissions for ACLing?
  • What should the default quota be for handlers?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment