# ModernIRC - The no-dependency Node.js library to make easy IRC bots

![Current Version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fcodeberg.org%2FChakSoft%2Fmodernirc%2Fraw%2Fbranch%2Fmain%2Fpackage.json&query=%24.version&style=for-the-badge&label=current%20version)
[![Ko-Fi](https://img.shields.io/badge/Support%20Us%20on%20Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/chaksoft)
[![Liberapay patrons](https://img.shields.io/liberapay/patrons/chaksoft?style=for-the-badge&logo=liberapay&label=LIBERAPAY%20PATRONS&link=https%3A%2F%2Fliberapay.com%2Fchaksoft%2Fdonate)](https://liberapay.com/chaksoft/donate)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/114540846754422568?domain=piaille.fr&style=for-the-badge&logo=mastodon)](https://piaille.fr/@chaksoft)
----

## Table of Contents

* [Installation](#installation)
* [Usage](#usage)
* [Configuration](#configuration)
  * [Uplink configuration](#uplink-configuration)
  * [Identity configuration](#identity-configuration)
  * [Auto Join](#auto-join)
  * [Context configuration](#context-configuration)
  * [Autostart configuration](#autostart-configuration)
* [Modules](#modules)
  * [Create a new module](#create-a-new-module)
    * [Module configuration](#module-configuration)
    * [Module commands](#module-commands)
    * [Module event bubbling](#module-event-bubbling)
    * [Message structure](#message-structure)
  * [Event Names](#event-names)
    * [Special Events](#special-events)
* [Logging](#logging)
  * [Child loggers](#child-loggers)
  * [Create custom transport](#create-custom-transport)
  * [Custom format](#custom-format)
* [HTTP Controller](#http-controller)
  * [Endpoints](#endpoints)
    * [`GET /identity`](#get-identity)
    * [`GET /channels`](#get-channels)
    * [`GET /modules`](#get-modules)
  * [Modifier endpoints](#modifier-endpoints)
    * [`POST /nick`](#post-nick)
    * [`POST /join`](#post-join)
    * [`POST /part`](#post-part)
    * [`POST /privmsg`](#post-privmsg)
    * [`POST /action`](#post-action)
    * [`POST /notice`](#post-notice)
    * [`POST /kick`](#post-kick)
    * [`POST /module`](#post-module)
* [Formatting Text](#formatting-text)
* [API Reference](#api-reference)
  * [Bot client object](#bot-client-object)
    * [`bot.send(str)`](#botsendstr)
    * [`bot.message(target, str)`](#botmessagetarget-str)
    * [`bot.notice(target, str)`](#botnoticetarget-str)
    * [`bot.join(channel, [key = null])`](#botjoinchannel-key--null)
    * [`bot.changeNickname(nickname)`](#botchangenicknamenickname)
    * [`bot.part(channel, [reason = ''])`](#botpartchannel-reason--)
    * [`bot.kick(channel, target, [reason = ''])`](#botkickchannel-target-reason--)
    * [`bot.me(channel, str)`](#botmechannel-action)
    * [`bot.joinedChannels`](#botjoinedchannels)
    * [`bot.serverConfig`](#botserverconfig)
    * [`bot.context`](#botcontext)
  * [Module options](#module-options)
    * [`options.logger`](#optionslogger)


## Installation

```shell
npm i modernirc
```

## Usage

```javascript
import { createBot } from 'modernirc'

async function main() {
  await createBot({
    uplink: {
      host: 'localhost',
      port: 6667,
    },
    autojoin: [{ name: '#Home' }],
  })
}

main().catch(console.error)
```

Congratulations, the bot is now started.

## Configuration

The configuration object that is passed as the first and only
argument of the `createBot` function has the following default properties :

```json
{
  "uplink": {
    "host": "localhost",
    "port": 6667,
    "password": null,
    "tls": false,
    "rejectUnauthorized": false,
    "autoPong": true
  },
  "identity": {
    "nickname": "modernbot",
    "username": "modernbot",
    "realname": "modernbot",
    "password": null
  },
  "autojoin": [],
  "modules": {},
  "logger": {},
  "api": {
    "enabled": false,
    "listen": {
      "port": 9999,
      "host": ""
    },
    "auth": {
      "key": null
    }
  },
  "context": {},
  "autostart": true
}
```

### Uplink configuration

The uplink configuration contains all the details of the connection to be made.

> The `host` property can be the domain name or the IP address of the IRC server.

### Identity configuration

On an IRC server, three properties are defining a user identity :
- the nickname: the displayed name in the users list
- the username: internal name used by the server to identify a user whatever the nickname is.
- the realname: "real" name (can contain spaces and accents unlink nickname and username)

The `password` property is used by the bot if there is a services operator (like Anope or Atheme)
to identify against a registered nickname. If the property is defined, the bot will wait for
a specific message from the NickServ service before sending the identify command.

### Auto Join

You can force the bot to automatically join channels at startup like this :

```javascript
{
  autojoin: [
    { name: '#Foo' },
  ]
}
```

And if the channel needs a key :

```javascript
{
  autojoin: [
    { name: '#Foo', key: 'bar' }
  ]
}
```

### Context configuration

You can specify in the options argument the context the bot will start from. This is related to the `bot.context`
object that allows you to embed whatever you want (i.e. database connectors).

### Autostart configuration

By default, the bot will start automatically at the end of the `createBot` process. If you want to control when
you're starting the bot, set this value to `false`. Please note that you'll have to manually call `bot.init()` to
actually connect to the IRC server.

## Modules

By default, the bot is doing just... nothing. It connects, takes some configuration
from the server first messages, but it will not react to anything.

This is the responsibility of the modules added to the bot.

### Create a new module

In your project folder, start by creating a `modules` folder at the root. It will contain all the
modules you'll add to the bot. Inside this new folder, create a new folder
which will contain the actual module you're creating.

In our example, we'll create a module that will greet every people joining a channel
where the bot is present.

```shell
mkdir -p modules/helloworld
```

```javascript
// modules/helloworld/index.js

export function init(bot, options) {
  return {
    onJoin(message) {
      if (message.prefix.nickname !== bot.identity.nickname) {
        bot.message(message.params[0], `Hello, ${message.prefix.nickname}!`)
      }
    },
  }
}
```
> The name of the function is built from the event name prefixed by `on`.
> For example, the `join` event will trigger the `onJoin` method, and a `privmsg` message will trigger a `onPrivmsg` method, etc.

#### Module configuration

You can configure your module in the configuration object :

```json
{
  "modules": {
    "hello": {
      "enabled": true,
      "configkey1": "configvalue1",
      "configkey2": 44
    }
  }
}
```

In our example, in the `hello` module, you can access `options.configkey1` and `options.configkey2`.

#### Module commands

You can configure specific commands for a module. They will be intercepted from PRIVMSG messages the bot is able
to receive.

First, you must specify the string prefix of the command, this way the bot will be able to know if the received
string is a command or not.

```json
{
  "modules": {
    "hello": {
      "enabled": true,
      "commandPrefix": "!",
      "configkey1": "configvalue1",
      "configkey2": 44
    }
  }
}
```

In this case, every received message starting with a `!` character will be considered as commands. It will trigger
a specific handler in the module : `onModuleCommand(moduleCommand: string, moduleArgs: string[], message: object)`.
The `moduleCommand` argument is parsed from the message, taking the first word of the message without the
command prefix.

For example :

```
// if the message is
!hello world
```

Then :

```javascript
export async function init(bot, options) {
  return {
    onModuleCommand(moduleCommand, moduleArgs, message) {
      // moduleCommand === 'hello'
      // moduleArgs === ['world']
    }
  }
}
```

> Please note that even if a message is considered a command, the `onPrivmsg` handler will still be triggered.

#### Module event bubbling

You can emit an event from a module and make it bubble to another module. In order to do that, the library
gives you an optional event emitter in the `init` function.

```javascript
export async function init(bot, options, emitter) {
  //...
}
```

The `emitter` is a built-in `EventEmitter` that can listen and emit events with embedded data. In the module
scope, it's not useful to listen to events here.

For example, if you have two modules : `moduleA` and `moduleB`. Let's presume we want to send an event from
moduleA to moduleB when a notice is received (yes, it's only an example).

```javascript
// modules/moduleA/index.js
export async function init(bot, options, emitter) {
  return {
    onNotice(message) {
      emitter.emit('bubble', 'moduleB', { foo: 'bar' })
    },
  }
}
```

To receive this in moduleB, we must first make it accept messages from moduleA :

```json
{
  "modules": {
    "moduleA": {
      "enabled": true,
    },
    "moduleB": {
      "enabled": true,
      "bubbleListeners": ["moduleA"]
    }
  }
}
```

Finally, we have to write the `onBubbleEvent` function in moduleB to receive the event.

```javascript
// modules/moduleB/index.js
export async function init(bot, options) {
  return {
    onBubbleEvent(source, ...args) {
      // source === 'moduleA'
      // args = [ { foo: 'bar' } ]
    },
  }
}
```

If you want to make a two-way communication, you'll need to add the `moduleB` in the bubbleListeners of moduleA as well.

#### Message structure

The message is a JSON object containing all the information relative to the incoming message, for example :

```text
:foo!foo@bar.baz PRIVMSG #Home :Hello World !
```

```json
{
  "prefix": {
    "nickname": "foo",
    "username": "foo",
    "hostname": "bar.baz"
  },
  "code": "PRIVMSG",
  "command": "privmsg",
  "params": ["#Home"],
  "message": "Hello World !",
  "self": false
}
```

The `self` property is set to `true` if the incoming message is a message from the bot itself. It can happen
on join and channel notice events.

### Event Names

IRC commands are alphanumeric codes that are translated in the library into
some human-readable events.

First, there is a special event that handles every message recieved from
the server, the `message` event.

Then, please find below the complete list of event names available:

| Command   | Event Name          |
|-----------|---------------------|
| `PRIVMSG` | `privmsg`           |
| `JOIN`    | `join`              |
| `QUIT`    | `quit`              |
| `PART`    | `part`              |
| `NOTICE`  | `notice`            |
| `NICK`    | `nick`              |
| `KICK`    | `kick`              |
| `TOPIC`   | `topic`             |
| `MODE`    | `mode`              |
| `INVITE`  | `invite`            |
| `ERROR`   | `error`             |
| `001`     | `welcome`           |
| `002`     | `yourhost`          |
| `003`     | `created`           |
| `004`     | `myinfo`            |
| `005`     | `serverconfig`      |
| `300`     | `none`              |
| `302`     | `userhost`          |
| `303`     | `ison`              |
| `301`     | `away`              |
| `305`     | `unaway`            |
| `306`     | `noaway`            |
| `311`     | `whoisuser`         |
| `312`     | `whoisserver`       |
| `313`     | `whoisoperator`     |
| `317`     | `whoisidle`         |
| `318`     | `endofwhois`        |
| `319`     | `whoischannels`     |
| `314`     | `whomasuser`        |
| `369`     | `endofwhowas`       |
| `321`     | `liststart`         |
| `322`     | `list`              |
| `323`     | `listend`           |
| `324`     | `channelmodeis`     |
| `325`     | `uniqopis`          |
| `331`     | `notopic`           |
| `332`     | `topic`             |
| `341`     | `inviting`          |
| `342`     | `summoning`         |
| `346`     | `invitelist`        |
| `347`     | `endofinvitelist`   |
| `348`     | `exceptlist`        |
| `349`     | `endofexceptlist`   |
| `351`     | `version`           |
| `352`     | `whoreply`          |
| `315`     | `endofwho`          |
| `353`     | `namreply`          |
| `366`     | `endofnames`        |
| `364`     | `links`             |
| `365`     | `endoflinks`        |
| `367`     | `banlist`           |
| `368`     | `endofbanlist`      |
| `371`     | `info`              |
| `374`     | `endofinfo`         |
| `375`     | `motdstart`         |
| `372`     | `motd`              |
| `376`     | `endofmotd`         |
| `381`     | `youreoper`         |
| `382`     | `rehashing`         |
| `383`     | `youreservice`      |
| `391`     | `time`              |
| `392`     | `usersstart`        |
| `393`     | `users`             |
| `394`     | `endofusers`        |
| `395`     | `nousers`           |
| `200`     | `tracelink`         |
| `201`     | `traceconnecting`   |
| `202`     | `tracehandshake`    |
| `203`     | `traceunknown`      |
| `204`     | `traceoperator`     |
| `205`     | `traceuser`         |
| `206`     | `traceserver`       |
| `207`     | `traceservice`      |
| `208`     | `tracenewtype`      |
| `209`     | `traceclass`        |
| `210`     | `tracereconnect`    |
| `261`     | `tracelog`          |
| `262`     | `traceend`          |
| `211`     | `statslinkinfo`     |
| `212`     | `statscommands`     |
| `213`     | `statscline`        |
| `214`     | `statsnline`        |
| `215`     | `statsiline`        |
| `216`     | `statskline`        |
| `218`     | `statsyline`        |
| `219`     | `endofstats`        |
| `241`     | `statsline`         |
| `242`     | `statsuptime`       |
| `243`     | `statsoline`        |
| `244`     | `statshline`        |
| `221`     | `umodeis`           |
| `251`     | `luserclient`       |
| `252`     | `luserop`           |
| `253`     | `luserunknown`      |
| `254`     | `luserchannels`     |
| `255`     | `luserme`           |
| `256`     | `adminme`           |
| `257`     | `adminloc1`         |
| `258`     | `adminloc2`         |
| `259`     | `adminemail`        |
| `263`     | `tryagain`          |
| `401`     | `nosuchnick`        |
| `402`     | `nosuchserver`      |
| `403`     | `nosuchchannel`     |
| `404`     | `cannotsendtochan`  |
| `405`     | `toomanychannels`   |
| `406`     | `wasnosuchnick`     |
| `407`     | `toomanytargets`    |
| `408`     | `nosuchservice`     |
| `409`     | `noorigin`          |
| `411`     | `norecipient`       |
| `412`     | `notexttosend`      |
| `413`     | `notoplevel`        |
| `414`     | `wildtoplevel`      |
| `421`     | `unknowncommand`    |
| `422`     | `nomotd`            |
| `423`     | `noadmininfo`       |
| `424`     | `fileerror`         |
| `431`     | `nonicknamegiven`   |
| `432`     | `erroneousnickname` |
| `433`     | `nicknameinuse`     |
| `436`     | `nickcollision`     |
| `437`     | `unavailresource`   |
| `441`     | `usernotinchannel`  |
| `442`     | `notonchannel`      |
| `443`     | `useronchannel`     |
| `444`     | `nologin`           |
| `445`     | `summondisabled`    |
| `446`     | `usersdisabled`     |
| `451`     | `notregistered`     |
| `461`     | `needmoreparams`    |
| `462`     | `alreadyregistered` |
| `463`     | `nopermforhost`     |
| `464`     | `passwdmismatch`    |
| `465`     | `yourebannedcreep`  |
| `467`     | `keyset`            |
| `471`     | `channelisfull`     |
| `472`     | `unknownmode`       |
| `473`     | `inviteonlychan`    |
| `474`     | `bannedfromchan`    |
| `475`     | `badchannelkey`     |
| `476`     | `badchanmask`       |
| `477`     | `nochanmodes`       |
| `478`     | `banlistfull`       |
| `481`     | `noprivileges`      |
| `482`     | `chanoprivsneeded`  |
| `483`     | `cantkillserver`    |
| `484`     | `restricted`        |
| `485`     | `uniqopprivsneeded` |
| `491`     | `nooperhost`        |
| `501`     | `umodeunknownflag`  |
| `502`     | `usersdontmatch`    |
| `209`     | `traceclass`        |
| `231`     | `serviceinfo`       |
| `232`     | `endofservices`     |
| `233`     | `service`           |
| `235`     | `servlistend`       |
| `316`     | `whoischanop`       |
| `362`     | `closing`           |
| `373`     | `infostart`         |
| `466`     | `youwillbebanned`   |
| `217`     | `statsqline`        |
| `234`     | `servlist`          |
| `361`     | `killdone`          |
| `363`     | `closeend`          |
| `384`     | `myportis`          |
| `476`     | `badchanmask`       |
| `492`     | `noservicehost`     |

All events can be ignored or handled according to what you want to do with
your bot.

> Please note that even if all RFC 1459/2812 events have been mapped in the library,
> some of them are only triggered between servers or for IRCOps, so
> your bot will never receive them.

### Special events

- `failed`: Event emitted by the bot client whenever an error code is received from the server. By default, it writes an error log.
- `action`: Triggered when a PRIVMSG is a CTCP ACTION message (`/me`). The CTCP marker is removed from the message before being passed to the event handler.

## Logging

A logger is embedded in the robot to allow unified logs. It can write logs in console, in a file
and to an HTTP request (as JSON or URL-encoded).

```json
{
  // ...
  "logger": {
    "level": "info",
    "levels": ["error", "warning", "info", "log", "debug"],
    "transports": [{
      "type": "console",
      "level": "debug",
      "opts": { "pretty": true }
    }],
    "name": "app"
  }
}
```

By default, it logs to the console every message with level more important than info, so info, warning and error.
You can customise the whole thing, and create custom transports if you need.

### Child loggers

When a module uses the embedded logger (`options.logger`), it will automatically append the name
of the module to the name in the configuration.

By default, the name is `app`. In a module `hello`, the `name` property will be `app/hello`.
You can create your own logger children if you want with the `child` method.

```javascript
// assuming the logger name is `app`
const child = logger.child('childName')

// this child will log with name `app/childName`
```

### Create custom transport

Transport in the ModernIRC logger is a function taking the `name` and the `opts` object. It returns an object
with a single function `output(_level: string, _raw: string)` doing the real job of outputting the log.

The `output` function can be sync or async, it will never be awaited to avoid I/O issues.

Example :

```javascript
// configuration
{
  //...
  logger: {
    //...
    transports: [{
      type: 'custom',
      level: 'debug',
      builder: (name, opts) => ({
        output(_level: string, _raw: string) {
          console.log(`${_level}: ${_raw}`)
        },
      }),
    }],
  },
}
```

### Custom format

If you don't want to write a full custom transport, but only format the output string,
you can customize the log string format by giving a `formatter` property to a default transport metadata.
A formatter is a function taking an object with the following properties :

- `date`: Date of the log
- `name`: Name of the logger (app or child)
- `level`: Log level
- `text`: Text message of the log

This function must return a **string**.

Example :

```javascript
{
  //...
  logger: {
    //...
    transports: [{
      type: 'console',
      level: 'debug',
      formatter: ({ date, name, level, text }) => `${level} ${date} - ${text}\n`,
    }],
  },
}
```

## HTTP Controller

You can control some actions of the bot from an HTTP interface. You must enable `api` in the configuration
first, and define an authentication key.

```json
{
  "api": {
    "enabled": true,
    "auth": {
      "key": "123456"
    }
  }
}
```

The key can be any string, but we recommend using UUIDs or a strong random bytes string.
For every call done to this HTTP gateway, the key must be set in an HTTP header `X-Key` and is
mandatory. If a call is done without this key, a 403 Forbidden will be returned.

### Endpoints

#### `GET /identity`

```json
{
  "nickname": "modernbot",
  "username": "modernbot",
  "realname": "modernbot"
}
```

This can be helpful to get the current identity of the bot.

> Note that if you change the nickname, this will change it there as well.

#### `GET /channels`

```json
{
  "#Bots": {
    "name": "#Bots",
    "users": [
      {
        "nickname": "chaksoft",
        "mode": ""
      },
      {
        "nickname": "modernbot",
        "mode": ""
      }
    ]
  }
}
```

Gets the full list of channels and users where the bot is present as well. This list is up-to-date
according to the different join, part, kick and quit events.

> Note that the bot will always be in all the users list.

#### `GET /modules`

```json
[
  {
    "name": "hello",
    "enabled": true
  }
]
```

Gets the list of all the modules in the bot configuration and if they are enabled or not. In the future,
the control API will be able to enable and disable modules on runtime.

### Modifier endpoints

All the modifiers are just shortcuts for sending commands. Please note that all these endpoints
will return 200 if the command has been successfully transmitted to the bot. But if the IRC server
refused the command for whatever reason, it will not be bubbled back to the API.

In case of 200, all below endpoints will return a simple JSON with `{ ok: true }`.

#### `POST /nick`

**JSON Body**

```json
{
  "nickname": "ModernBot2"
}
```

Changes the nickname.

#### `POST /join`

**JSON Body**

```json
{
  "channel": "#Foo",
  "key": "bar"
}
```

Joins a channel, `key` is optional if there is no key for the channel.

#### `POST /part`

**JSON Body**

```json
{
  "channel": "#Foo"
}
```

Leaves the channel. If the bot is not in the channel, this command does nothing.

#### `POST /privmsg`

**JSON Body**

```json
{
  "target": "#Bots",
  "message": "Hello there !"
}
```

Sends a message to the designated target.

#### `POST /action`

**JSON Body**

```json
{
  "target": "#Bots",
  "action": "is eating some flowers..."
}
```

Sends a CTCP ACTION message to the designated target.

#### `POST /notice`

**JSON Body**

```json
{
  "target": "chaksoft",
  "message": "hello there, this is a notice !"
}
```

Sends a notice to the designated target.

#### `POST /kick`

**JSON Body**

```json
{
  "channel": "#Bots",
  "target": "chaksoft",
  "reason": "Chop Chop !"
}
```

Kicks the designated target from the designated channel. Please note that if the bot has not enough
privileges, the command will do nothing.

#### `POST /module`

**JSON Body**

```json
{
  "moduleName": "hello",
  "someparam": "somevalue"
}
```

Sends a message to a specific module. The `moduleName` property is mandatory, if empty it will return a 404 error.
The other properties of the body depends on what the module is waiting for in its `onApiRequest` handler.

```javascript
export async function init(bot, options) {
  return {
    onApiRequest(body) {
      // do something here with the body (will not contain the `moduleName` property)
    },
  }
}
```

The `onApiRequest` can return a JSON object that will be serialized into the HTTP response (in the `result` property).
If you return nothing, the response will be `{ "ok": true, "result": null }`.

## Formatting Text

The `modernirc` package is also exporting utility functions for coloring messages,
and also a color reference object.

> Please note that styled texts could be not seen by other users depending
> on their clients. Some clients will simply ignore the color formatting
> and will just display the text.

### `Colors: {[x: string]: string}`

Constant object you can use to set the colors in the utility functions.

| Name         | Color value |
|--------------|-------------|
| `White`      | `00`        |
| `Black`      | `01`        |
| `Blue`       | `02`        |
| `Green`      | `03`        |
| `Red`        | `04`        |
| `Brown`      | `05`        |
| `Magenta`    | `06`        |
| `Orange`     | `07`        |
| `Yellow`     | `08`        |
| `LightGreen` | `09`        |
| `Cyan`       | `10`        |
| `LightCyan`  | `11`        |
| `LightBlue`  | `12`        |
| `Pink`       | `13`        |
| `Grey`       | `14`        |
| `LightGrey`  | `15`        |
| `Ansi52`     | `16`        |
| `Ansi94`     | `17`        |
| `Ansi100`    | `18`        |
| `Ansi58`     | `19`        |
| `Ansi22`     | `20`        |
| `Ansi29`     | `21`        |
| `Ansi23`     | `22`        |
| `Ansi24`     | `23`        |
| `Ansi17`     | `24`        |
| `Ansi54`     | `25`        |
| `Ansi53`     | `26`        |
| `Ansi89`     | `27`        |
| `Ansi88`     | `28`        |
| `Ansi130`    | `29`        |
| `Ansi142`    | `30`        |
| `Ansi64`     | `31`        |
| `Ansi28`     | `32`        |
| `Ansi35`     | `33`        |
| `Ansi30`     | `34`        |
| `Ansi25`     | `35`        |
| `Ansi18`     | `36`        |
| `Ansi91`     | `37`        |
| `Ansi90`     | `38`        |
| `Ansi125`    | `39`        |
| `Ansi124`    | `40`        |
| `Ansi166`    | `41`        |
| `Ansi184`    | `42`        |
| `Ansi106`    | `43`        |
| `Ansi34`     | `44`        |
| `Ansi49`     | `45`        |
| `Ansi37`     | `46`        |
| `Ansi33`     | `47`        |
| `Ansi19`     | `48`        |
| `Ansi129`    | `49`        |
| `Ansi127`    | `50`        |
| `Ansi161`    | `51`        |
| `Ansi196`    | `52`        |
| `Ansi208`    | `53`        |
| `Ansi226`    | `54`        |
| `Ansi154`    | `55`        |
| `Ansi46`     | `56`        |
| `Ansi86`     | `57`        |
| `Ansi51`     | `58`        |
| `Ansi75`     | `59`        |
| `Ansi21`     | `60`        |
| `Ansi171`    | `61`        |
| `Ansi201`    | `62`        |
| `Ansi198`    | `63`        |
| `Ansi203`    | `64`        |
| `Ansi215`    | `65`        |
| `Ansi227`    | `66`        |
| `Ansi191`    | `67`        |
| `Ansi83`     | `68`        |
| `Ansi122`    | `69`        |
| `Ansi87`     | `70`        |
| `Ansi111`    | `71`        |
| `Ansi63`     | `72`        |
| `Ansi177`    | `73`        |
| `Ansi207`    | `74`        |
| `Ansi205`    | `75`        |
| `Ansi217`    | `76`        |
| `Ansi223`    | `77`        |
| `Ansi229`    | `78`        |
| `Ansi193`    | `79`        |
| `Ansi157`    | `80`        |
| `Ansi158`    | `81`        |
| `Ansi159`    | `82`        |
| `Ansi153`    | `83`        |
| `Ansi147`    | `84`        |
| `Ansi183`    | `85`        |
| `Ansi219`    | `86`        |
| `Ansi212`    | `87`        |
| `Ansi16`     | `88`        |
| `Ansi233`    | `89`        |
| `Ansi235`    | `90`        |
| `Ansi237`    | `91`        |
| `Ansi239`    | `92`        |
| `Ansi241`    | `93`        |
| `Ansi244`    | `94`        |
| `Ansi247`    | `95`        |
| `Ansi250`    | `96`        |
| `Ansi254`    | `97`        |
| `Ansi231`    | `98`        |

See https://modern.ircdocs.horse/formatting.html#colors-16-98 for complete
Ansi colors reference.

**Example usage in a module**

```javascript
import { Colors, color, bold } from 'modernirc'

export async function init(bot, options) {
  return {
    onJoin(message) {
      bot.message(
        message.params[0],
        bold(`Welcome, ${color(message.prefix.nickname, Colors.Ansi17)} !`)
      )
    },
  }
}
```

### `color(text: string, foreground: string, background: string = null) -> string`

This function can be used to color a text. The foreground and background colors
must be color values.

### `hexColor(text: string, rgb: string) -> string`

This function can be used to color a text. It's an experimental feature on IRC
clients that could not be supported.

The `foreground` parameter must be a 6-char hex string representing the RGB
code of the color you want to apply.

### `bold(text: string) -> string`

Sets a text as bold.

### `italic(text: string) -> string`

Sets a text as italic.

### `underline(text: string) -> string`

Sets a text as underlined.

### `strikethrough(text: string) -> string`

Sets a text as striked.

### `monospace(text: string) -> string`

Sets a text as forced monospace font.

## API Reference

### Bot client object

#### `bot.send(str)`

Sends a raw command string to the server.

#### `bot.message(target, str)`

Sends a text message to the target. Target can be a channel or a
private channel with another user.

#### `bot.notice(target, str)`

Sends a notice to the target. Target usually is a user but can also
be a channel.

#### `bot.join(channel, [key = null])`

Joins a channel with the given key if applicable.
Check for the error codes in the table above to check for available answers.

#### `bot.changeNickname(nickname)`

Changes the nickname of the bot. This change is temporary and is not saved.
When the bot restarts, it will take the configured identity to register itself.

#### `bot.part(channel, [reason = ''])`

Makes the bot leave a specified channel and optionally a reason.

#### `bot.kick(channel, target, [reason = ''])`

Kicks a target (usually a user) from a channel. Please note that this
command will do nothing if the bot has no operator rights in the specified
channel. Otherwise, the `CHANOPRIVSNEEDED` error is sent by the server as
a failure notification.

#### `bot.me(channel, action)`

Sends a CTCP ACTION message to the given channel.
This command is equivalent to send a `/me` command.

#### `bot.joinedChannels`

Array of channels where the bot is present. The list of users inside each channel is kept updated following the part, kick and quit events.

#### `bot.serverConfig`

Object containing all the supported modes and flags from the server.
At startup, a server will send a full list of all supported modes and specific flags enabled.

#### `bot.context`

The context object is an empty object that can handle anything you want. It can be useful when using
a database connector, or some external configuration you may want to use through the events and the modules.

> The context is **always** a reference. Be careful if you modify the context in a module, it'll modify
> it for other modules as well.

### Module options

#### `options.logger`

This property contains the child logger affected to the module. In order to have unified logs, it's
recommended to use this one instead of `console.log`.

## License

This software is licensed under [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.html).
