diff --git a/.gitignore b/.gitignore index fc161b4..5e3c3eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.servers.lua -examples/itte*.lua -!sample.servers.lua +examples/*/itte*.lua +!hellobot.servers.lua +!ramenkan.sample.servers.lua diff --git a/README.md b/README.md index fb1540e..a844490 100644 --- a/README.md +++ b/README.md @@ -7,33 +7,16 @@ Currently supported: - Authentication via SASL (plain) or Nickserv - Joining multiple servers +- Ad-hoc connecting to servers and joining channels - Config reload -## Requirements +## Installation -- [Lua 5.x](https://www.lua.org/) -- [luasocket](https://w3.impa.br/~diego/software/luasocket/) -- [luasec](https://github.com/brunoos/luasec) - - -## Usage - -See the bot files in the `examples/` directory for usage notes. - - -## Example: ramenkan - -- Install Lua and other dependencies, e.g. for Alpine: - `apk add lua-socket lua-sec` - -- Copy the `itte*.lua` files to the `examples/` directory. Copy the - `examples/sample.servers.lua` as `ramenkan.servers.lua` and change the server - settings as applicable. - -- Run: `nohup lua /path/to/examples/ramenkan.lua >/dev/null 2>&1 &` +Please see the [docs](docs/README.md). ## License -BSD-3.0 +[BSD-3.0](https://opensource.org/licenses/BSD-3-Clause) + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ea80f34 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,149 @@ +# Itte Documentation + + +## Requirements + +- [Lua 5.x](https://www.lua.org/) +- [luasocket](https://w3.impa.br/~diego/software/luasocket/) +- [luasec](https://github.com/brunoos/luasec) + + +## Installation + +- Install Lua and other dependencies. Example: + + - Debian/Ubuntu-based distributions: `apt-get install lua-socket lua-sec` + + - Alpine Linux: `apk add lua-socket lua-sec` + +- Copy the `itte*.lua` files to the project directory. + + +## Usage + +- Create two config files, or copy the sample files from `examples/` to the + same directory level as the `itte*.lua` files. Replace instances of + `[prefix]` hereafter with the name of your choice without spaces or special + characters, e.g. the project name. + + - `[prefix].servers.lua`: list servers and global admins here. + + - `[prefix].config.lua`: add bot settings and handlers here. + +- There are two variables in `*.servers.lua`: + + - `itte_servers` (required): add a server to `itte_server` following the + example configurations. + + - `itte_admins` (optional): this is a table of global admins who can access + bot-wide handlers like connecting servers. If left undefined, global + admin-only handlers will not be run. In some cases this may be desirable to + reduce incidents of bot tampering. + +- There are two optional variables in `*.config.lua`: + + - `itte_config`: customise bot settings, such as the default response and + error messages shown to users. + + - `itte_handlers`: most bots listen for some pre-defined code words (often + known as "commands") and respond by calling a handler, or function to + handle the request accordingly. + + The module will refer to the keywords as codes to distinguish them + from IRC commands sent to the server like `JOIN` or `PART`. Name the + handler after the code word that will be used to trigger the function and + add it to `itte_handlers` so it can be automatically picked up by the + module. For example, if the code users will type is `!hello` (where `!` is + a prefix to mark it as a bot code), name the function + `itte_handlers.hello()`. Handler names cannot start with numbers or special + characters. + +- Initialise the module in a project file named `[prefix].lua` and add the + following to the file: + + ``` + -- Import the module + local itte = require("itte") + + -- Set the prefix for the module to find the config files + -- The prefix is the name preceding `.servers.lua` and `.config.lua` + itte.confs.prefix = "[prefix]" + + -- Call the run function + itte.run() + ``` + +- Start the bot: `lua ./[prefix].lua` + + +## Built-in service codes + +The module includes a number of built-in codes that bots can respond to. The +full list can be viewed on IRC by issuing the code `!help` in a private +message with a bot or in a channel where the bot is present. Admin users will +also see admin-only codes to manage a bot. + +Global admin-only codes: + +- `connect [server] [user] [password]`: connect to server listed in the server + config, where `[server]` is the name given in the servers table (or use + `servers` to get the names). + +- `quit [server] [user] [password]`: disconnect from a server. The bot process + will automatically exit if it is not connected to any server. + +- `reload [user] [password]`: reload the bot config. + +- `servers [user] [password]`: list known/active servers from the config. + +Server admin-only codes: + +- `channels [password]`: list known channels from the server config. + +- `join [channel] [password]`: join channel(s), e.g. `!join #bots #programming + password`. + +- `part [channel] [passowrd]`: leave channel(s), e.g. `!part #bots password`. + +Codes accessible by all users: + +- `help`: list available service codes. + +- `ping`: send a pong message. + + +## Running a bot as a background process + +There are multiple ways to have a bot run in the background. + +### nohup + +Using the `nohup` command is quick and simple, though it does not support +restarting if the bot exits due to a network issue. + +To run the bot and discard any output by redirecting it to `/dev/null`: +`nohup lua /path/to/[prefix].lua >/dev/null 2>&1 &` + +### Init scripts, e.g. systemd + +Startup scripts enable services to start automatically and relaunch in the +event of an unexpected exit (unless expressly stopped through an init system +command). The instructions below describe setting up and running a [systemd] +service for a bot under a non-privileged user. + +- Create a systemd directory for the user running the bot if it does not + already exist: `mkdir -p /home/[user]/.config/systemd/user` + +- Copy the sample `*.service` to the user systemd directory and rename the file + to `[prefix].service`. Edit the service description, `WorkingDirectory` and + `ExecStart` values as needed. Use full paths for the `WorkingDirectory` and + location of the Lua executable, or the service file will not work. + +- Start and enable the service: + + ``` + systemctl --user start [prefix] + systemctl --user enable [prefix] + ``` + +[systemd]: https://www.freedesktop.org/software/systemd/man/systemd.service.html diff --git a/examples/hellobot/hellobot.config.lua b/examples/hellobot/hellobot.config.lua new file mode 100644 index 0000000..57fa8bf --- /dev/null +++ b/examples/hellobot/hellobot.config.lua @@ -0,0 +1,81 @@ +-- --------------------------------------------------------------------------- +-- Custom variables +-- --------------------------------------------------------------------------- + +local hello_words = { + "ahoy!", "EHLO", "hai", "hello!", "henlo", "hey!", "hi!", "ACK (TCP)", + "aloha! (Hawaiian)", "ciao! (Italian)", "hallo (Dutch)", "hej (Swedish)", + "hola (Spanish)", "salut! (French)", "saluton (Esperanto)", "tag! (German)", + "toki! (Toki Pona)", "هلا (pron: hala)", "مااس (Arabic, pron: salaam)", + "สวัสดี (Thai, pron: sawatdee)", "你好 (Chinese, pron: ni hao)", + "こんにちわ (Japanese, pron: konnichiwa)", "안녕 (Korean, pron: annyeong)", + "नमस्ते (Hindi, pron: namaste)", "ᐊᐃ (Ai)", "Привет (Russian, pron: preevyet)", + "שָׁלוֹם (Hebrew, pron: shalom)", "kia ora (Māori)" +} + + +-- --------------------------------------------------------------------------- +-- Config +-- --------------------------------------------------------------------------- + +-- [[ +-- `itte_config` +-- Add custom settings and override the default config settings here (see +-- `itte.lua` for the full list). Settings need to be appended to this variable +-- to be collected by the reload function. +-- ]] +itte_config = { + debug = true, +} + + +-- --------------------------------------------------------------------------- +-- Handlers +-- --------------------------------------------------------------------------- + +-- Load the main module. +local irc = require("itte") +-- Load the util module to use some helper functions. +local util = require("itteutil") + + +--[[ +-- `itte_handlers` +-- Define any custom handlers in a new table, then assign the table to it. +-- Handlers can also be added to it directly e.g. `itte_handlers.hello()`. +-- Handler names should correspond to the service code name, e.g. a function +-- `itte_handlers.hello()` will be called within chat as +-- `[command_prefix]hello`, as in `!hello`. +-- +-- Here an object called `h` is created for convenience, and functions will be +-- added to it. +--]] +local h = {} + + +-- Reply with a random greeting. +-- The handler takes two parameters, `cxt` and `msg`. +-- +-- The first is a context table that includes server details and a reference to +-- the socket connection. The other is a message table, which is the IRC user +-- message broken down into parts such as the sender, recipient, reply_to, and +-- the code trigger used. +-- +-- The context and message tables are required for the `message()` function to +-- know which server and user to direct a response. +-- +-- The `message()` function takes the server connection from the context table, +-- a table of users or channels to reply to, and the text string, then formats +-- the string into an IRC command and sends it to the server. +-- +-- `util.pick()` is a helper function that takes a table of items and picks one +-- randomly by default if the number of items is unspecified. It will return +-- the items in a table, so `[1]` will get the one (and in this case, only) +-- value in the table. +function h.hello(cxt, msg) + irc.message(cxt, { msg.reply_to }, util.pick(hello_words)[1]) +end + + +-- Hook up the handlers. +itte_handlers = h diff --git a/examples/ramenkan.lua b/examples/hellobot/hellobot.lua similarity index 91% rename from examples/ramenkan.lua rename to examples/hellobot/hellobot.lua index 304bc70..7b7e373 100644 --- a/examples/ramenkan.lua +++ b/examples/hellobot/hellobot.lua @@ -1,5 +1,5 @@ -- Initialise module. -local ramenkan = require("itte") +local hellobot = require("itte") -- [[ -- Set the config file paths. @@ -20,7 +20,7 @@ local ramenkan = require("itte") -- Either set confs.prefix, *or* a combination of confs.config and -- confs.server, but not both. -- ]] -ramenkan.confs.prefix = "ramenkan" +hellobot.confs.prefix = "hellobot" -- Call the run function. -ramenkan.run() +hellobot.run() diff --git a/examples/sample.servers.lua b/examples/hellobot/hellobot.servers.lua similarity index 84% rename from examples/sample.servers.lua rename to examples/hellobot/hellobot.servers.lua index d06962b..4ab8182 100644 --- a/examples/sample.servers.lua +++ b/examples/hellobot/hellobot.servers.lua @@ -9,7 +9,7 @@ -- -- This variable is optional, but certain handlers like server disconnection -- will not work if it is unset. -itte_admins = { globaladmin = "password", } +itte_admins = { demo = "password", } -- `itte_servers` @@ -22,13 +22,13 @@ itte_servers = { server_name = { host = "irc.example.tld", port = 6667, - channels = { "#channel-name" }, - cap = { "sasl" }, + channels = { "#channel1", "#channel2" }, + cap = nil, nick = "botnick", auth_type = nil, auth_user = "botuser", - auth_pass = "password", + auth_pass = nil, code_prefix = "!", - admins = { adminuser = "password", }, + admins = { demouser = "password", }, }, } diff --git a/examples/hellobot/hellobot.service b/examples/hellobot/hellobot.service new file mode 100644 index 0000000..78212a6 --- /dev/null +++ b/examples/hellobot/hellobot.service @@ -0,0 +1,11 @@ +[Unit] +Description=hellobot — an IRC bot + +[Service] +WorkingDirectory=/path/to/hellobot +ExecStart=/usr/bin/lua ./hellobot.lua +Restart=always +RestartSec=300 + +[Install] +WantedBy=default.target diff --git a/examples/ramenkan.config.lua b/examples/ramenkan/ramenkan.config.lua similarity index 96% rename from examples/ramenkan.config.lua rename to examples/ramenkan/ramenkan.config.lua index 7092baa..5467bd6 100644 --- a/examples/ramenkan.config.lua +++ b/examples/ramenkan/ramenkan.config.lua @@ -2,10 +2,6 @@ -- Ramen data -- --------------------------------------------------------------------------- --- [[ --- This is sample data and could be split into another file. --- ]] - local ramen = {} ramen.noodle_broth_types = { @@ -259,7 +255,6 @@ ramen.links = { -- Ramen functions -- --------------------------------------------------------------------------- --- Load the util module early to use some helper functions. local util = require("itteutil") @@ -373,12 +368,6 @@ end -- Config -- --------------------------------------------------------------------------- --- [[ --- `itte_config` --- Add custom settings and override the default config settings here. --- Settings need to be appened to this variable to be collected by the reload --- function. --- ]] itte_config = { debug = true, messages = { @@ -428,18 +417,7 @@ itte_config.messages.water = { -- Handlers -- --------------------------------------------------------------------------- --- Load the module. local irc = require("itte") - - ---[[ --- `itte_handlers` --- Define any custom handlers in a new table, then assign the table to it. --- Handlers can also be added to it directly e.g. `itte_handlers.hello()`. --- Handler names should correspond to the service code name, e.g. a function --- `itte_handlers.hello()` will be called within chat as --- `[command_prefix]hello`, as in `!hello`. ---]] local h = {} diff --git a/examples/ramenkan/ramenkan.lua b/examples/ramenkan/ramenkan.lua new file mode 100644 index 0000000..416424c --- /dev/null +++ b/examples/ramenkan/ramenkan.lua @@ -0,0 +1,5 @@ +local ramenkan = require("itte") + +ramenkan.confs.prefix = "ramenkan" + +ramenkan.run() diff --git a/examples/ramenkan/ramenkan.sample.servers.lua b/examples/ramenkan/ramenkan.sample.servers.lua new file mode 100644 index 0000000..29b17cf --- /dev/null +++ b/examples/ramenkan/ramenkan.sample.servers.lua @@ -0,0 +1,34 @@ +itte_admins = { USER = "PASSWORD" } + +itte_servers = { + town = { + host = "localhost", + port = 6697, + channels = { "#ramenkan", "#bots" }, + nick = "ramenkan", + auth_user = "USER", + code_prefix = "!", + admins = { USER = "PASSWORD" }, + }, + casa = { + host = "m455.casa", + port = 6697, + channels = { "#kitchen", "#siliconpals" }, + cap = { "sasl", "draft/chathistory" }, + nick = "ramenkan", + auth_type = "sasl", + auth_user = "USER", + auth_pass = "PASSWORD", + code_prefix = "!", + admins = { USER = "PASSWORD" }, + }, + tildechat = { + host = "irc.tilde.chat", + port = 6697, + channels = { "#bots" }, + nick = "ramenkan", + auth_user = "USER", + code_prefix = "!", + admins = { USER = "PASSWORD" }, + }, +} diff --git a/examples/ramenkan/ramenkan.service b/examples/ramenkan/ramenkan.service new file mode 100644 index 0000000..d474512 --- /dev/null +++ b/examples/ramenkan/ramenkan.service @@ -0,0 +1,11 @@ +[Unit] +Description=ramenkan — a ramen-themed IRC bot + +[Service] +WorkingDirectory=%h/bin/itte/examples/ramenkan +ExecStart=/usr/bin/lua ./ramenkan.lua +Restart=always +RestartSec=300 + +[Install] +WantedBy=default.target diff --git a/itte.lua b/itte.lua index 6eb30a6..52518fe 100644 --- a/itte.lua +++ b/itte.lua @@ -126,7 +126,18 @@ function itte.get_config(reload) -- Update config with value overrides from itte_config if itte_config ~= nil then for k, v in pairs(itte_config) do - itte.config[k] = v + -- Second-level nested tables + if type(v) == "table" then + for kk, vv in pairs(v) do + if (type(vv) == "table") or (type(vv) == "function") then + itte.config[k] = v + else + itte.config[k][kk] = vv + end + end + else + itte.config[k] = v + end end end if itte_handlers ~= nil then itte.handlers = itte_handlers end