# Configuration

Options live in `editable/config.lua`. Server hooks are in `editable/server.lua` and client hooks in `editable/client.lua`. Restart the resource after a change.

## General

| Option | Default | What it does |
| --- | --- | --- |
| `Config.Locale` | `'en'` | `en` or `pl` (`shared/locales.lua`) |
| `Config.Debug` | `false` | Console logs |
| `Config.OpenKey` | `'T'` | Default key. Players can rebind it in GTA settings, Key Bindings, FiveM |
| `Config.Mouse` | `true` | Show the cursor while the chat is open (tabs, emoji, settings) |
| `Config.MaxLength` | `256` | Maximum characters per message |
| `Config.MaxMessages` | `150` | Messages kept in the UI |
| `Config.FadeOut` | `10` | Seconds after the last message before the feed hides. `0` = never hide |
| `Config.Position` | `'top-left'` | `'top-left'`, `'top-right'`, `'middle-left'` or `'bottom-left'` |
| `Config.Timestamps` | `true` | `false` = never show timestamps. When `true` players can still hide them |
| `Config.HideInPauseMenu` | `true` | Hide the chat in the pause menu |
| `Config.AllowColorCodes` | `false` | Let players use `^1` to `^9` colour codes |
| `Config.DefaultChannel` | `'local'` | Channel for plain text (no command) typed on the All tab or on a tab the player cannot write to |

## Theme

```lua title="editable/config.lua"
Config.Theme = {
    style = 'appearance',
    accent = nil,
    playerChoice = true,
}
```

| Key | Description |
| --- | --- |
| `style` | `appearance`, `velmor`, `velmor2`, `grids`, `modern`, `emerald`, `samp`, `nightshift`, `discord`, `twitch`, `bubble` or `terminal` |
| `accent` | Overrides the accent colour of the style, for example `'#FF9B00'`. `nil` = the colour of the style |
| `playerChoice` | Players can pick style, main colour (24 colour palette), position (3x3 grid) and size in the chat settings |

## Speech bubbles

```lua title="editable/config.lua"
Config.Bubbles = {
    enabled = true,
    max = 6,
    distance = 18.0,
    duration = 7000,
    showSelf = true,
    channels = { ['local'] = true },
}
```

| Key | Description |
| --- | --- |
| `enabled` | Bubbles above heads: an animated `...` while typing and messages that type out letter by letter |
| `max` | Bubbles drawn at once. Each one is a small browser texture |
| `distance` | Draw distance in metres |
| `duration` | Milliseconds a message stays above the head |
| `showSelf` | Show your own bubble |
| `channels` | Plain messages on these channels also get a bubble. Messages from commands with `overhead = true` always get one |

## Overhead text and typing indicator

```lua title="editable/config.lua"
Config.OverheadText = { enabled = true, duration = 7000, distance = 15.0 }
Config.TypingIndicator = { enabled = true, distance = 12.0 }
```

`OverheadText` is the plain GTA 3D text for `/me`, `/do` and `/try`. It is used when `Config.Bubbles.enabled` is `false`.
`TypingIndicator` shows `...` above players who have the chat open, as a bubble or as 3D text. It uses the player state `kojaTyping`.

## Images

```lua title="editable/config.lua"
Config.Images = {
    enabled = true,
    hosts = { 'i.imgur.com', 'cdn.discordapp.com', 'media.discordapp.net', 'media.tenor.com', 'i.ibb.co' },
    paste = {
        enabled = true,
        maxSide = 720,
        maxKB = 180,
        cooldown = 10,
    },
}
```

| Key | Description |
| --- | --- |
| `enabled` | Image previews for links |
| `hosts` | Only links from these hosts get a preview, so nobody can log player IPs with their own server |
| `paste.enabled` | Ctrl+V a copied image straight into the chat. It is shrunk and compressed in the UI and sent through a latent event, nothing is uploaded anywhere |
| `paste.maxSide` | Longer side in pixels, scaled down to this |
| `paste.maxKB` | Maximum size after compression. The server rejects bigger images |
| `paste.cooldown` | Seconds between images per player. Staff ignore it |

The server accepts only PNG, JPEG and WebP data, never URLs, SVG or HTML.

## Binds

```lua title="editable/config.lua"
Config.Binds = { enabled = true, max = 20 }
```

`enabled = false` turns off `/bind`, `/unbind` and `/binds`. `max` is the number of binds per player.

## Developer mode

```lua title="editable/config.lua"
Config.DevMode = {
    enabled = false,
    ace = 'koja-chat.dev',
    execute = false,
    backlog = 200,
    clientConsole = true,
}
```

| Key | Description |
| --- | --- |
| `enabled` | Players with the ACE get a Console tab with the live server console. Errors are red, click a line to copy |
| `ace` | The ACE a developer needs |
| `execute` | Anything typed in the Console tab runs as a server console command. Off by default |
| `backlog` | Lines sent to a developer when they join |
| `clientConsole` | Also mirrors your own F8 console into the tab when the client supports it |

The Console tab needs `RegisterConsoleListener` on the server build. Without it the console prints a warning and the tab only shows the commands. Executed commands are printed to the server console and logged to Discord under `console`.

```cfg title="server.cfg"
add_ace group.admin koja-chat.dev allow
```

## Permissions

| Option | Default | What it does |
| --- | --- | --- |
| `Config.AdminGroups` | `nil` | Passed to `KOJA.Server.IsAdmin`, for example `{ 'admin', 'superadmin' }`. `nil` = the koja-lib `Config.Admin` |
| `Config.AdminAce` | `nil` | ACE that makes a player staff, for example `'koja-chat.admin'` |
| `Config.StaffBadge` | `{ label = 'STAFF', color = '#ef4444' }` | Badge next to staff names on the OOC and Local tabs. `nil` disables it |

## Departments

```lua title="editable/config.lua"
Config.Departments = {
    police = { label = 'LSPD', color = '#3b82f6', jobs = { 'police', 'sheriff' } },
    ems = { label = 'EMS', color = '#ef4444', jobs = { 'ambulance' } },
    mechanic = { label = 'MECH', color = '#f59e0b', jobs = { 'mechanic' } },
}
Config.DepartmentsRequireDuty = false
```

A channel with `department = true` (the `/dep` command) shows the department label and colour of the player, and players only see messages of their own department. `DepartmentsRequireDuty = true` puts a player in the department only while on duty. Tabs are refreshed when the job or duty changes on ESX and QBCore.

## Channels

Each entry is a tab. The order is the tab order.

```lua title="editable/config.lua"
Config.Channels = {
    { id = 'local', label = 'ch_local', color = '#a78bfa', icon = 'users', range = 20.0 },
    { id = 'ooc', label = 'ch_ooc', color = '#94a3b8', icon = 'globe', nameMode = 'player' },
    { id = 'social', label = 'ch_social', color = '#38bdf8', icon = 'megaphone' },
    { id = 'dep', label = 'ch_dep', color = '#3b82f6', icon = 'shield', department = true },
    { id = 'admin', label = 'ch_admin', color = '#ef4444', icon = 'crown', admin = true, nameMode = 'player' },
    { id = 'system', label = 'ch_system', color = '#f59e0b', icon = 'bell', readonly = true, hidden = true },
}
```

| Key | Description |
| --- | --- |
| `id` | Channel id used by commands and the API |
| `label` | A locale key (`ch_local`) or plain text |
| `color`, `icon` | Tab colour and icon |
| `range` | Proximity in metres. Players further away do not receive the message |
| `admin` | Only staff can see and write |
| `jobs` | Only these jobs can see and write |
| `department` | Split by `Config.Departments` |
| `readonly` | Players cannot write. Scripts still can |
| `nameMode` | `'character'` (character name) or `'player'` (FiveM name) |
| `hidden` | No tab. System messages are shown on every tab anyway |

Icons: `users`, `globe`, `megaphone`, `shield`, `crown`, `bell`, `at`, `mask`, `mail`, `flag`, `info`, `alert`, `radio`, `siren`, `star`, `heart`, `wrench`, `newspaper`, `sparkles`, `clock`, `mute`.

Other resources add channels with [`RegisterChannel`](/new/chat/developer-api).

## Commands

`Config.Commands` is keyed by command name. Set `enabled = false` to remove one. A command with a `channel` posts its text to that channel:

| Key | Description |
| --- | --- |
| `channel` | Channel the text is posted to |
| `style` | `default`, `action`, `describe`, `try`, `twitter`, `anon` or `ad` |
| `tag`, `color` | Label and colour of the message |
| `overhead` | Show text or a bubble above the player |
| `range` | Overrides the range of the channel, in metres |
| `anonymous` | Hide the author. Staff still see the ID |
| `cost`, `account` | Money taken per message from `'cash'` or `'bank'` |
| `cooldown` | Seconds between uses. Staff ignore it |
| `chance` | `/try` only: success chance in percent (default `50`) |
| `duration` | `/poll` only: seconds a poll stays open (default `120`) |
| `admin` | Only staff can use it |
| `params` | Parameter names shown in the suggestions (locale keys such as `p_message`) |
| `name` | Use another command name than the key |

The other commands (`pm`, `r`, `report`, `mute`, `poll` and so on) have their own logic in `server/commands.lua`, and commands with `client = true` run on the client. See [Commands](/new/chat/commands) for the defaults.

## Moderation

Staff bypass all of it.

```lua title="editable/config.lua"
Config.AntiSpam = {
    enabled = true,
    interval = 700,
    burst = 5,
    window = 8,
    duplicate = true,
    duplicateWindow = 15,
    autoMute = { strikes = 5, minutes = 5 },
}

Config.Filter = {
    enabled = true,
    block = false,
    replace = '*',
    blockLinks = false,
    words = {},
}
```

| Key | Description |
| --- | --- |
| `AntiSpam.interval` | Minimum milliseconds between two messages |
| `AntiSpam.burst`, `window` | At most `burst` messages in `window` seconds |
| `AntiSpam.duplicate`, `duplicateWindow` | Blocks the same message twice in a row within this many seconds |
| `AntiSpam.autoMute` | Mutes after `strikes` blocked messages within a minute of each other, for `minutes`. `nil` disables it |
| `Filter.block` | `true` rejects the message, `false` replaces the word |
| `Filter.replace` | Replacement character, repeated for each letter |
| `Filter.blockLinks` | Rejects `http://`, `https://`, `www.` and `discord.gg` |
| `Filter.words` | Blocked words, case-insensitive, matched anywhere in the text |

`/slowmode` works even when `AntiSpam.enabled` is `false`. Mutes are stored by license in the resource KVP, so they survive reconnects and restarts.

## Discord logs

```lua title="editable/config.lua"
Config.Logs = {
    enabled = false,
    webhook = '',
    channels = {},
    pm = true,
    reports = true,
    moderation = true,
}
```

| Key | Description |
| --- | --- |
| `enabled` | Master switch. Logs are sent through `KOJA.Server.LogMessage` |
| `webhook` | Default webhook for everything |
| `channels` | Per channel override: a webhook URL, or `false` to not log that channel. The key is the channel id, `'admin'` for example |
| `pm`, `reports` | Log private messages and reports |
| `moderation` | Log mutes, unmutes, slow mode, clearing, announcements and deleted messages |

Every entry shows the player, the channel, the character name and the license and Discord identifiers.

## Editable files

**`editable/server.lua`**

| Function | What it does |
| --- | --- |
| `Custom.IsAdmin(src)` | Who counts as staff. Default: `KOJA.Server.IsAdmin(src, Config.AdminGroups, Config.AdminAce)` |
| `Custom.GetName(src, info, channel)` | Name printed next to a message. Default: the character name, or the FiveM name when the channel has `nameMode = 'player'` |
| `Custom.GetBadges(src, info, channelId)` | Badges before the name, `{ { label, color } }`. Default: the staff badge on OOC and Local |

`info` is `{ id, name, player, job, jobLabel, grade, gradeLabel, onDuty, department, admin }`.

**`editable/client.lua`**

| Function | What it does |
| --- | --- |
| `CustomClient.CanOpen()` | Return `false` to block opening (handcuffed, dead, in a cutscene) |
| `CustomClient.OnOpen()` | Called when the chat opens |
| `CustomClient.OnClose()` | Called when the chat closes |
