commit dc26ebe7a487f05ae841dea77b9f989e39eab781
parent ce022a132e5f60f9fb6126d8abc84100b2f38adb
Author: Andreas Gruhler <agruhl@gmx.ch>
Date: Wed, 13 Aug 2025 23:52:41 +0200
feat(thelounge): add irc web client
Diffstat:
4 files changed, 655 insertions(+), 0 deletions(-)
diff --git a/hcl/default/thelounge/data-volume.hcl b/hcl/default/thelounge/data-volume.hcl
@@ -0,0 +1,31 @@
+# Register external nfs volume with Nomad CSI
+# https://www.nomadproject.io/docs/commands/volume/register
+type = "csi"
+# Unique ID of the volume, volume.source field in a job
+id = "thelounge"
+# Display name of the volume.
+name = "thelounge"
+# ID of the physical volume from the storage provider
+external_id = "csi-thelounge"
+plugin_id = "nfs"
+
+# You must provide at least one capability block
+# You must provide a block for each capability
+# youintend to use in a job's volume block
+# https://www.nomadproject.io/docs/commands/volume/register
+capability {
+ access_mode = "multi-node-multi-writer"
+ attachment_mode = "file-system"
+}
+
+# https://github.com/kubernetes-csi/csi-driver-nfs/blob/master/docs/driver-parameters.md
+context {
+ server = "turris"
+ share = "csi-thelounge"
+}
+
+mount_options {
+ # mount.nfs: Either use '-o nolock' to keep locks local, or start statd.
+ mount_flags = ["nolock"]
+}
+
diff --git a/hcl/default/thelounge/templates/config.js.tmpl b/hcl/default/thelounge/templates/config.js.tmpl
@@ -0,0 +1,517 @@
+// https://github.com/thelounge/thelounge/blob/master/defaults/config.js
+
+"use strict";
+
+module.exports = {
+ // ## Server settings
+
+ // ### `public`
+ //
+ // When set to `true`, The Lounge starts in public mode. When set to `false`,
+ // it starts in private mode.
+ //
+ // - A **public server** does not require authentication. Anyone can connect
+ // to IRC networks in this mode. All IRC connections and channel
+ // scrollbacks are lost when a user leaves the client.
+ // - A **private server** requires users to log in. Their IRC connections are
+ // kept even when they are not using or logged in to the client. All joined
+ // channels and scrollbacks are available when they come back.
+ //
+ // This value is set to `false` by default.
+ public: false,
+
+ // ### `host`
+ //
+ // IP address or hostname for the web server to listen to. For example, set it
+ // to `"127.0.0.1"` to accept connections from localhost only.
+ //
+ // For UNIX domain sockets, use `"unix:/absolute/path/to/file.sock"`.
+ //
+ // This value is set to `undefined` by default to listen on all interfaces.
+ host: "0.0.0.0",
+
+ // ### `port`
+ //
+ // Set the port to listen to.
+ //
+ // This value is set to `9000` by default.
+ port: {{ env "NOMAD_PORT_http" }},
+
+ // ### `bind`
+ //
+ // Set the local IP to bind to for outgoing connections.
+ //
+ // This value is set to `undefined` by default to let the operating system
+ // pick its preferred one.
+ bind: undefined,
+
+ // ### `reverseProxy`
+ //
+ // When set to `true`, The Lounge is marked as served behind a reverse proxy
+ // and will honor the `X-Forwarded-For` header.
+ //
+ // This value is set to `false` by default.
+ reverseProxy: true,
+
+ // ### `maxHistory`
+ //
+ // Defines the maximum number of history lines that will be kept in memory per
+ // channel/query, in order to reduce the memory usage of the server. Setting
+ // this to `-1` will keep unlimited amount.
+ //
+ // This value is set to `10000` by default.
+ maxHistory: 10000,
+
+ // ### `https`
+ //
+ // These settings are used to run The Lounge's web server using encrypted TLS.
+ //
+ // If you want more control over the webserver,
+ // [use a reverse proxy instead](https://thelounge.chat/docs/guides/reverse-proxies).
+ //
+ // The available keys for the `https` object are:
+ //
+ // - `enable`: when set to `false`, HTTPS support is disabled
+ // and all other values are ignored.
+ // - `key`: Path to the private key file.
+ // - `certificate`: Path to the certificate.
+ // - `ca`: Path to the CA bundle.
+ //
+ // The value of `enable` is set to `false` to disable HTTPS by default, in
+ // which case the other two string settings are ignored.
+ https: {
+ enable: false,
+ key: "",
+ certificate: "",
+ ca: "",
+ },
+
+ // ## Client settings
+
+ // ### `theme`
+ //
+ // Set the default theme to serve to new users. They will be able to select a
+ // different one in their client settings among those available.
+ //
+ // The Lounge ships with two themes (`default` and `morning`) and can be
+ // extended by installing more themes. Read more about how to manage them
+ // [here](https://thelounge.chat/docs/guides/theme-creation).
+ //
+ // This value needs to be the package name and not the display name. For
+ // example, the value for Morning would be `morning`, and the value for
+ // Solarized would be `thelounge-theme-solarized`.
+ //
+ // This value is set to `"default"` by default.
+ theme: "default",
+
+ // ### `prefetch`
+ //
+ // When set to `true`, The Lounge will load thumbnails and site descriptions
+ // from URLs posted in channels and private messages.
+ //
+ // This value is set to `false` by default.
+ prefetch: false,
+
+ // ### `disableMediaPreview`
+ //
+ // When set to `true`, The Lounge will not preview media (images, video and
+ // audio) hosted on third-party sites. This ensures the client does not
+ // make any requests to external sites. If `prefetchStorage` is enabled,
+ // images proxied via the The Lounge will be previewed.
+ //
+ // This has no effect if `prefetch` is set to `false`.
+ //
+ // This value is set to `false` by default.
+ disableMediaPreview: false,
+
+ // ### `prefetchStorage`
+
+ // When set to `true`, The Lounge will store and proxy prefetched images and
+ // thumbnails on the filesystem rather than directly display the content at
+ // the original URLs.
+ //
+ // This option primarily exists to resolve mixed content warnings by not
+ // loading images from http hosts. This option does not work for video
+ // or audio as The Lounge will only load these from https hosts.
+ //
+ // If storage is enabled, The Lounge will fetch and store images and thumbnails
+ // in the `${THELOUNGE_HOME}/storage` folder.
+ //
+ // Images are deleted when they are no longer referenced by any message
+ // (controlled by `maxHistory`), and the folder is cleaned up when The Lounge
+ // restarts.
+ //
+ // This value is set to `false` by default.
+ prefetchStorage: false,
+
+ // ### `prefetchMaxImageSize`
+ //
+ // When `prefetch` is enabled, images will only be displayed if their file
+ // size does not exceed this limit.
+ //
+ // This value is set to `2048` kilobytes by default.
+ prefetchMaxImageSize: 2048,
+
+ // ### prefetchMaxSearchSize
+ //
+ // This value sets the maximum response size allowed when finding the Open
+ // Graph tags for link previews. The entire response is temporarily stored
+ // in memory and for some sites like YouTube this can easily exceed 300
+ // kilobytes.
+ //
+ // This value is set to `50` kilobytes by default.
+ prefetchMaxSearchSize: 50,
+
+ // ### `prefetchTimeout`
+ //
+ // When `prefetch` is enabled, this value sets the number of milliseconds
+ // before The Lounge gives up attempting to fetch a link. This can be useful
+ // if you've increased the `prefetchMaxImageSize`.
+ //
+ // Take caution, however, that an inordinately large value may lead to
+ // performance issues or even a denial of service, since The Lounge will not
+ // be able to clean up outgoing connections as quickly. Usually the default
+ // value is appropriate, so only change it if necessary.
+ //
+ // This value is set to `5000` milliseconds by default.
+ prefetchTimeout: 5000,
+
+ // ### `fileUpload`
+ //
+ // Allow uploading files to the server hosting The Lounge.
+ //
+ // Files are stored in the `${THELOUNGE_HOME}/uploads` folder, do not expire,
+ // and are not removed by The Lounge. This may cause issues depending on your
+ // hardware, for example in terms of disk usage.
+ //
+ // The available keys for the `fileUpload` object are:
+ //
+ // - `enable`: When set to `true`, files can be uploaded on the client with a
+ // drag-and-drop or using the upload dialog.
+ // - `maxFileSize`: When file upload is enabled, users sending files above
+ // this limit will be prompted with an error message in their browser. A value of
+ // `-1` disables the file size limit and allows files of any size. **Use at
+ // your own risk.** This value is set to `10240` kilobytes by default.
+ // - `baseUrl`: If you want to change the URL where uploaded files are accessed,
+ // you can set this option to `"https://example.com/folder/"` and the final URL
+ // would look like `"https://example.com/folder/aabbccddeeff1234/name.png"`.
+ // If you use this option, you must have a reverse proxy configured,
+ // to correctly proxy the uploads URLs back to The Lounge.
+ // This value is set to `null` by default.
+ fileUpload: {
+ enable: false,
+ maxFileSize: 10240,
+ baseUrl: null,
+ },
+
+ // ### `transports`
+ //
+ // Set `socket.io` transports.
+ //
+ // This value is set to `["polling", "websocket"]` by default.
+ transports: ["polling", "websocket"],
+
+ // ### `leaveMessage`
+ //
+ // Set users' default `quit` and `part` messages if they are not providing
+ // one.
+ //
+ // This value is set to `"The Lounge - https://thelounge.chat"` by
+ // default.
+ leaveMessage: "The Lounge - https://thelounge.chat",
+
+ // ## Default network
+
+ // ### `defaults`
+ //
+ // Specifies default network information that will be used as placeholder
+ // values in the *Connect* window.
+ //
+ // The available keys for the `defaults` object are:
+ //
+ // - `name`: Name to display in the channel list of The Lounge. This value is
+ // not forwarded to the IRC network.
+ // - `host`: IP address or hostname of the IRC server.
+ // - `port`: Usually 6667 for unencrypted connections and 6697 for
+ // connections encrypted with TLS.
+ // - `password`: Connection password. If the server supports SASL capability,
+ // then this password will be used in SASL authentication.
+ // - `tls`: Enable TLS connections
+ // - `rejectUnauthorized`: Whether the server certificate should be verified
+ // against the list of supplied Certificate Authorities (CAs) by your
+ // Node.js installation.
+ // - `nick`: Nick name. Percent signs (`%`) will be replaced by random
+ // numbers from 0 to 9. For example, `Guest%%%` may become `Guest123`.
+ // - `username`: User name.
+ // - `realname`: Real name displayed by some clients. Defaults to the nick if set to ""
+ // - `leaveMessage`: Network specific leave message (overrides global leaveMessage)
+ // - `join`: Comma-separated list of channels to auto-join once connected.
+ //
+ // This value is set to connect to the official channel of The Lounge on
+ // Libera.Chat by default:
+ //
+ // ```js
+ // defaults: {
+ // name: "Libera.Chat",
+ // host: "irc.libera.chat",
+ // port: 6697,
+ // password: "",
+ // tls: true,
+ // rejectUnauthorized: true,
+ // nick: "thelounge%%",
+ // username: "thelounge",
+ // realname: "The Lounge User",
+ // join: "#thelounge"
+ // }
+ // ```
+ defaults: {
+ name: "Soju",
+ host: "oc.in0rdr.ch",
+ port: 6697,
+ password: "",
+ tls: true,
+ rejectUnauthorized: true,
+ nick: "thelounge%%",
+ username: "thelounge",
+ realname: "",
+ leaveMessage: "",
+ join: ""
+ },
+
+ // ### `lockNetwork`
+ //
+ // When set to `true`, users will not be able to modify host, port and TLS
+ // settings and will be limited to the configured network.
+ // These fields will also be hidden from the UI.
+ //
+ // This value is set to `false` by default.
+ lockNetwork: false,
+
+ // ## User management
+
+ // ### `messageStorage`
+
+ // The Lounge can log user messages, for example to access them later or to
+ // reload messages on server restart.
+
+ // Set this array with one or multiple values to enable logging:
+ // - `text`: Messages per network and channel will be stored as text files.
+ // **Messages will not be reloaded on restart.**
+ // - `sqlite`: Messages are stored in SQLite database files, one per user.
+ //
+ // Logging can be disabled globally by setting this value to an empty array
+ // `[]`. Logging is also controlled per user individually in the `log` key of
+ // their JSON configuration file.
+ //
+ // This value is set to `["sqlite", "text"]` by default.
+ messageStorage: ["sqlite", "text"],
+
+ // ### `storagePolicy`
+
+ // When the sqlite storage is in use, control the maximum storage duration.
+ // A background task will periodically clean up messages older than the limit.
+
+ // The available keys for the `storagePolicy` object are:
+ //
+ // - `enabled`: If this is false, the cleaning task is not running.
+ // - `maxAgeDays`: Maximum age of an entry in days.
+ // - `deletionPolicy`: Controls what types of messages are being deleted.
+ // Valid options are:
+ // - `statusOnly`: Only delete message types which are status related (e.g. away, back, join, parts, mode, ctcp...)
+ // but keep actual messages from nicks. This keeps the DB size down while retaining "precious" messages.
+ // - `everything`: Delete everything, including messages from irc nicks
+ storagePolicy: {
+ enabled: false,
+ maxAgeDays: 7,
+ deletionPolicy: "statusOnly",
+ },
+
+ // ### `useHexIp`
+ //
+ // When set to `true`, users' IP addresses will be encoded as hex.
+ //
+ // This is done to share the real user IP address with the server for host
+ // masking purposes. This is encoded in the `username` field and only supports
+ // IPv4.
+ //
+ // This value is set to `false` by default.
+ useHexIp: false,
+
+ // ## WEBIRC support
+ //
+ // When enabled, The Lounge will pass the connecting user's host and IP to the
+ // IRC server. Note that this requires to obtain a password from the IRC
+ // network that The Lounge will be connecting to and generally involves a lot
+ // of trust from the network you are connecting to.
+ //
+ // There are 2 ways to configure the `webirc` setting:
+ //
+ // - **Basic**: an object where keys are IRC hosts and values are passwords.
+ // For example:
+ //
+ // ```json
+ // webirc: {
+ // "irc.example.net": "thisiswebircpassword1",
+ // "irc.example.org": "thisiswebircpassword2",
+ // },
+ // ```
+ //
+ // - **Advanced**: an object where keys are IRC hosts and values are functions
+ // that take two arguments (`webircObj`, `network`) and return an
+ // object to be directly passed to `irc-framework`. `webircObj` contains the
+ // generated object which you can modify. For example:
+ //
+ // ```js
+ // webirc: {
+ // "irc.example.com": (webircObj, network) => {
+ // webircObj.password = "thisiswebircpassword";
+ // webircObj.hostname = `webirc/${webircObj.hostname}`;
+ // return webircObj;
+ // },
+ // },
+ // ```
+ //
+ // This value is set to `null` to disable WEBIRC by default.
+ webirc: null,
+
+ // ## identd and oidentd support
+
+ // ### `identd`
+ //
+ // Run The Lounge with `identd` support.
+ //
+ // The available keys for the `identd` object are:
+ //
+ // - `enable`: When `true`, the identd daemon runs on server start.
+ // - `port`: Port to listen for ident requests.
+ //
+ // The value of `enable` is set to `false` to disable `identd` support by
+ // default, in which case the value of `port` is ignored. The default value of
+ // `port` is 113.
+ identd: {
+ enable: false,
+ port: 113,
+ },
+
+ // ### `oidentd`
+ //
+ // When this setting is a string, this enables `oidentd` support using the
+ // configuration file located at the given path.
+ //
+ // This is set to `null` by default to disable `oidentd` support.
+ oidentd: null,
+
+ // ## LDAP support
+
+ // These settings enable and configure LDAP authentication.
+ //
+ // They are only being used in private mode. To know more about private mode,
+ // see the `public` setting above.
+
+ //
+ // The authentication process works as follows:
+ //
+ // 1. The Lounge connects to the LDAP server with its system credentials.
+ // 2. It performs an LDAP search query to find the full DN associated to the
+ // user requesting to log in.
+ // 3. The Lounge tries to connect a second time, but this time using the
+ // user's DN and password. Authentication is validated if and only if this
+ // connection is successful.
+ //
+ // The search query takes a couple of parameters in `searchDN`:
+ //
+ // - a base DN `searchDN/base`. Only children nodes of this DN will likely
+ // be returned;
+ // - a search scope `searchDN/scope` (see LDAP documentation);
+ // - the query itself, built as `(&(<primaryKey>=<username>) <filter>)`
+ // where `<username>` is the user name provided in the log in request,
+ // `<primaryKey>` is provided by the config and `<filter>` is a filtering
+ // complement also given in the config, to filter for instance only for
+ // nodes of type `inetOrgPerson`, or whatever LDAP search allows.
+ //
+ // Alternatively, you can specify the `bindDN` parameter. This will make The
+ // Lounge ignore `searchDN` options and assume that the user DN is always
+ // `<bindDN>,<primaryKey>=<username>`, where `<username>` is the user name
+ // provided in the log in request, and `<bindDN>` and `<primaryKey>` are
+ // provided by the configuration.
+ //
+ // The available keys for the `ldap` object are:
+ ldap: {
+ // - `enable`: when set to `false`, LDAP support is disabled and all other
+ // values are ignored.
+ enable: false,
+
+ // - `url`: A url of the form `ldaps://<ip>:<port>`.
+ // For plain connections, use the `ldap` scheme.
+ url: "ldaps://example.com",
+
+ // - `tlsOptions`: LDAP connection TLS options (only used if scheme is
+ // `ldaps://`). It is an object whose values are Node.js' `tls.connect()`
+ // options. It is set to `{}` by default.
+ // For example, this option can be used in order to force the use of IPv6:
+ // ```js
+ // {
+ // host: 'my::ip::v6',
+ // servername: 'example.com'
+ // }
+ // ```
+ tlsOptions: {},
+
+ // - `primaryKey`: LDAP primary key. It is set to `"uid"` by default.
+ primaryKey: "uid",
+
+ // - `baseDN`: LDAP base DN, alternative to `searchDN`. For example, set it
+ // to `"ou=accounts,dc=example,dc=com"`.
+ // When unset, the LDAP auth logic with use `searchDN` instead to locate users.
+
+ // - `searchDN`: LDAP search DN settings. This defines the procedure by
+ // which The Lounge first looks for the user DN before authenticating them.
+ // It is ignored if `baseDN` is specified. It is an object with the
+ // following keys:
+ searchDN: {
+ // - `rootDN`: This bind DN is used to query the server for the DN of
+ // the user. This is supposed to be a system user that has access in
+ // read-only to the DNs of the people that are allowed to log in.
+ // It is set to `"cn=thelounge,ou=system-users,dc=example,dc=com"` by
+ // default.
+ rootDN: "cn=thelounge,ou=system-users,dc=example,dc=com",
+
+ // - `rootPassword`: Password of The Lounge LDAP system user.
+ rootPassword: "1234",
+
+ // - `filter`: it is set to `"(&(objectClass=person)(memberOf=ou=accounts,dc=example,dc=com))"`
+ // by default.
+ filter: "(&(objectClass=person)(memberOf=ou=accounts,dc=example,dc=com))",
+
+ // - `base`: LDAP search base (search only within this node). It is set
+ // to `"dc=example,dc=com"` by default.
+ base: "dc=example,dc=com",
+
+ // - `scope`: LDAP search scope. It is set to `"sub"` by default.
+ scope: "sub",
+ },
+ },
+
+ // ## Debugging settings
+
+ // The `debug` object contains several settings to enable debugging in The
+ // Lounge. Use them to learn more about an issue you are noticing but be aware
+ // this may produce more logging or may affect connection performance so it is
+ // not recommended to use them by default.
+ //
+ // All values in the `debug` object are set to `false`.
+ debug: {
+ // ### `debug.ircFramework`
+ //
+ // When set to true, this enables extra debugging output provided by
+ // [`irc-framework`](https://github.com/kiwiirc/irc-framework), the
+ // underlying IRC library for Node.js used by The Lounge.
+ ircFramework: false,
+
+ // ### `debug.raw`
+ //
+ // When set to `true`, this enables logging of raw IRC messages into each
+ // server window, displayed on the client.
+ raw: false,
+ },
+};
diff --git a/hcl/default/thelounge/templates/nginx.conf.tmpl b/hcl/default/thelounge/templates/nginx.conf.tmpl
@@ -0,0 +1,14 @@
+server {
+ listen {{ env "NOMAD_PORT_https" }} ssl;
+
+ ssl_certificate /etc/letsencrypt/live/irc.in0rdr.ch/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/irc.in0rdr.ch/privkey.pem;
+
+ location / {
+ proxy_pass http://{{ env "NOMAD_ADDR_http" }};
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
diff --git a/hcl/default/thelounge/thelounge.nomad b/hcl/default/thelounge/thelounge.nomad
@@ -0,0 +1,93 @@
+# https://github.com/thelounge/thelounge-docker/blob/master/docker-compose.yml
+# https://thelounge.chat/docs/install-and-upgrade#docker
+
+job "thelounge" {
+ datacenters = ["dc1"]
+
+ vault {}
+
+ group "server" {
+ count = 1
+
+ volume "tls" {
+ type = "csi"
+ source = "certbot"
+ access_mode = "multi-node-multi-writer"
+ attachment_mode = "file-system"
+ }
+ volume "thelounge" {
+ type = "csi"
+ source = "thelounge"
+ access_mode = "multi-node-multi-writer"
+ attachment_mode = "file-system"
+ }
+
+ network {
+ port "http" {
+ to = 9000
+ }
+ port "https" {
+ static = 44412
+ }
+ }
+
+ task "nginx" {
+ driver = "podman"
+
+ config {
+ image = "docker.io/library/nginx:stable-alpine"
+ ports = ["https"]
+ volumes = [
+ # mount the templated config from the task directory to the container
+ "local/thelounge.conf:/etc/nginx/conf.d/thelounge.conf",
+ ]
+ }
+
+ volume_mount {
+ volume = "tls"
+ destination = "/etc/letsencrypt"
+ }
+
+ template {
+ destination = "${NOMAD_TASK_DIR}/thelounge.conf"
+ data = file("./templates/nginx.conf.tmpl")
+ }
+
+ resources {
+ memory = 50
+ memory_max = 256
+ cpu = 200
+ }
+ }
+
+ task "thelounge" {
+ driver = "podman"
+
+ config {
+ image = "ghcr.io/thelounge/thelounge:latest"
+ force_pull = true
+ ports = ["http"]
+ volumes = [
+ # mount the templated config from the task directory to the container
+ "local/config.js:/etc/thelounge/config.js",
+ ]
+ }
+
+ volume_mount {
+ volume = "thelounge"
+ destination = "/var/opt/thelounge"
+ }
+
+ template {
+ destination = "${NOMAD_TASK_DIR}/config.js"
+ data = file("./templates/config.js.tmpl")
+ }
+
+ resources {
+ memory = 512
+ memory_max = 1024
+ cpu = 500
+ }
+ }
+ }
+}