Setting up the soju IRC bouncer on OpenBSD, and using it with Emacs' ERC
Setting up the soju[a] IRC bouncer[b] on my OpenBSD server was fairly straightforward. It's not currently in ports, so i downloaded a release from sourcehut:
soju is written in Go, but soju releases contain a Makefile that allows one to do `make && make install`. After doing that, i created a `_soju` group and user via groupadd(8) and useradd(8).
i then created an rc script for soju.
#!/bin/ksh daemon="/usr/local/bin/soju" daemon_user="_soju" daemon_logger="daemon.info" . /etc/rc.d/rc.subr rc_reload=NO rc_cmd $1
i then created a `/run/soju` directory, chown(8)'d it to `_soju:_soju`, and after editing `/etc/soju/config` appropriately, ran the following commands:
# sojudb create-user admin -admin # chown -R _soju:_soju /var/lib/soju # sojuctl user create -username [username] -password [password] # rcctl start soju # sojuctl user run [username] network create -addr ircs://irc.oftc.net -name oftc -nick [nick] -connect-command "PRIVMSG NickServ :IDENTIFY [password]"
Et voilá: this was enough to get soju working with the IRC app on my phone (Revolution IRC[c]).
However, getting Emacs' ERC to work with it was not so straightforward. The main problem is that ERC, as provided by Emacs 28.3:
- doesn't handle SASL authentication to an IRC server;
- doesn't distinguish between the username for an IRC server and the nick(s) one uses on that server.
The latter is a problem when ERC tries to automatically reconnect after a disconnect: it will fail, due to trying to use the nick associated with a given network, rather than the soju username.
Working around these issues involved:
- Downloading and installing `erc-sasl.el`, from the Spacemacs distro:
- Adding the following to my Emacs config:
(defvar erc-soju-server "[host]" "Hostname running soju instance.") (defvar erc-soju-port [port] "Port on which soju is running.") (defvar erc-soju-username "[soju username]" "Username on soju instance.") (defvar erc-soju-networks '([list of networks]) "Networks available for `erc-soju-username' on soju instance.") (defvar erc-soju-networks-channels '(("[network name as reported by network]" . ([list of channels])) [...])) "Association list of channels on each network to join by default.") (defvar erc-soju t "Whether ERC is being used with soju.") ;; Modify erc-login to support SASL. ;; https://libera.chat/guides/emacs-erc (load-file "~/.config/emacs/erc-sasl.el") (add-to-list 'erc-sasl-server-regexp-list erc-soju-server) (defun erc-login () "Perform user authentication at the IRC server. (PATCHED)" (erc-log (format "login: nick: %s, user: %s %s %s :%s" (erc-current-nick) (user-login-name) (or erc-system-name (system-name)) erc-session-server erc-session-user-full-name)) (if erc-session-password (erc-server-send (format "PASS %s" erc-session-password)) (message "Logging in without password")) (when (and (featurep 'erc-sasl) (erc-sasl-use-sasl-p)) (erc-server-send "CAP REQ :sasl")) (erc-server-send (format "NICK %s" (erc-current-nick))) (erc-server-send (format "USER %s %s %s :%s" ;; hacked - S.B. (if erc-anonymous-login erc-email-userid (user-login-name)) "0" "*" erc-session-user-full-name)) (erc-update-mode-line)) ;; Modify erc-server-reconnect to behave differently with soju. (defun erc-server-reconnect () "Reestablish the current IRC connection. Make sure you are in an ERC buffer when running this." (let ((buffer (erc-server-buffer))) (unless (buffer-live-p buffer) (if (eq major-mode 'erc-mode) (setq buffer (current-buffer)) (error "Reconnect must be run from an ERC buffer"))) (with-current-buffer buffer (erc-update-mode-line) (erc-set-active-buffer (current-buffer)) (setq erc-server-last-sent-time 0) (setq erc-server-lines-sent 0) (let ((erc-server-connect-function (or erc-session-connector #'erc-open-network-stream))) (erc-open erc-session-server erc-session-port (if erc-soju erc-soju-username erc-server-current-nick) erc-session-user-full-name t erc-session-password nil nil nil erc-session-client-certificate (if erc-soju erc-soju-username erc-session-username) (erc-networks--id-given erc-networks--id)) (unless (with-suppressed-warnings ((obsolete erc-reuse-buffers)) erc-reuse-buffers) (cl-assert (not (eq buffer (current-buffer))))))))) (defun erc-soju-start (arg) "Start an ERC session with soju. With prefix arg, close channel buffers and terminate processes." (interactive "P") (let ((choice (completing-read "[Soju] Network? " erc-soju-networks))) (setq erc-soju t) ;;(setq erc-autojoin-mode nil) (if arg (dolist (bfr (buffer-list)) (if (and (string= "erc-mode" (with-current-buffer bfr major-mode)) (string-match (concat "^#" "\\|" "^" erc-soju-server) (buffer-name bfr))) (let ((kill-buffer-query-functions nil)) (kill-buffer bfr))))) (setq erc-email-userid (concat erc-soju-username "/" choice "@emacs")) (erc-tls :server erc-soju-server :port erc-soju-port :nick erc-soju-username))) (defun erc-soju-after-connect (_server _nick) "Join channels specified by `erc-soju-networks-channels'." (let ((network (assoc erc-network erc-soju-networks-channels))) (if network (dolist (chan (cdr network)) (erc-join-channel chan))))) (add-hook 'erc-after-connect #'erc-soju-after-connect)
Whew! But having done all that, ERC seems to now be working with soju.☙
[a] i chose soju after initially trying to use ZNC, which i chose after doing some research into (a) recommended IRC bouncers that were (b) also available as OpenBSD ports. Once i actually started trying to use it, i encountered an issue which led me to this page:
Thus educated, i felt it best to consider other options. The main option i found was the combination of pounce(1) and calico(1):
This felt like it was probably the best option overall, but it also felt like the setup was too complicated for someone as-yet unfamiliar with IRC bouncers. i thus decided to go with manually setting up soju, as the bouncer used by sourcehut.
[b] An “IRC bouncer” is a particular type of Bounced Network Connection (BNC):
Many BNCs remain connected to an IRC server in the event the client should disconnect from the Internet. Often state changes are tracked so that they may be relayed to the client upon reconnection. Some implementations opt to store all messages sent across the network that the client would have normally received and send them upon the client's reconnection; this is often considered to be much too resource dependent for commercial hosting services to provide.
[c] Revolution IRC is available from F-Droid: