Emacs Jabber

Some notes about Emacs Jabber.

Main repository: https://codeberg.org/emacs-jabber/emacs-jabber

My repository: https://gitlab.com/cnngimenez/emacs-jabber

1. A simple chatbot in Emacs Jabber

This script sends a message and disconnects from the server. A cronjob can execute it at specific times.

1.1. Starting script

I made this script to work with Emacs in Termux.

  #! /usr/bin/env -S emacs -x
;;; script.el --- One line description  -*- lexical-binding: t; -*-

;; Copyright 2025 Christian Gimenez
;;
;; Author: Christian Gimenez
;; Maintainer: Christian Gimenez
;; Version: 0.1.0
;; Keywords: comm
;; URL:
;; Package-Requires: ((emacs "27.1"))

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.


;;; Commentary:

;; 

;;; Code:

;; Add emacs-jabber paths
(add-to-list 'load-path "/data/data/com.termux/files/home/repos/emacs/emacs-jabber/")
(add-to-list 'load-path "/data/data/com.termux/files/home/repos/emacs/emacs-jabber/lisp")
(add-to-list 'load-path "/data/data/com.termux/files/home/repos/emacs/emacs-jabber/lisp/jabber-fallback-lib")
(require 'jabber)

;; Import the processing data functions.
;; The following data is required:
;; * `message-make' function that makes the string message to send.
;;
;; Variables/constants expected: `my-message-botname', `my-message-muc-to',
;; `my-message-subject', `my-message-test-to'.
;;
;; The variable `my-message-connection-data' is an alist with the following:
;;   '((username . "USERNAME") (server . "SERVER") (resource . "bot")
;;     (password . "ACCOUNT-PASSWORD"))
(add-to-list 'load-path default-directory)
(require 'my-message)

(defun my-send-message (jc)
  "Send a message.
JC is a current and established jabber connection.

Once connected send and message and do chatbot stuff.

Useful as a hook."
  (message "Sending message now.")
  ;; Join to a MUC before sending. Else, the server may answer with error.
  ;; (jabber-muc-join jc my-message-muc-to my-message-botname)

  (let ((message (my-message-make)))
    (sleep-for 2)
    (jabber-send-message jc my-message-test-to my-message-subject message "chat")
    ;; (jabber-send-message jc my-message-muc-to my-message-subject message "groupchat")
    (sleep-for 5))

  (jabber-disconnect-one jc)
  (message "Jabber message sent."))

;; --------------------------------------------------
;; Setup Jabber to only connect and send the message

;; (setq jabber-debug-log-xml "~/xmpp-bot.log") ;; for debugging purpaoses.
(setq jabber-post-connect-hooks nil)  ;; Removing presences, MUC auto-join, etc.
(add-hook 'jabber-post-connect-hooks #'my-send-message)

(message "Starting connection.")
(jabber-connect (alist-get 'username my-message-connection-data)
                (alist-get 'server my-message-connection-data)
                (alist-get 'resource my-message-connection-data)
                nil ;; registerp
                (alist-get 'password my-message-connection-data))
;; Wait for disconnection
(message "Waiting for message and disconnection.")
(while jabber-connections
  ;; Wait until `jabber-connections' is empty. The `jabber-disconnect-one'
  ;; function deletes the jabber connection from the variable.
  (sleep-for 1))

(provide 'script)
;;; script.el ends here

1.2. The script you should edit

  ;;; my-message.el --- Message  -*- lexical-binding: t; -*-

;; Copyright 2025 Christian Gimenez
;;
;; Author: Christian Gimenez
;; Maintainer: Christian Gimenez
;; Version: 0.1.0
;; Keywords: comm
;; URL:
;; Package-Requires: ((emacs "27.1"))

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.


;;; Commentary:

;; Make the XMPP message.

;;; Code:
(defconst my-message-botname "My bot")
(defconst my-message-muc-to "the-group@muc.server.org")
(defconst my-message-test-to "someone@server.org")
(defconst my-message-subject "Message subject")
(defconst my-message-connection-data '((username . "myusername")
                                (server . "server.org")
                                (resource . "the-bot")
                                (password . "mypassword")))



(defun my-message-make ()
  "Format my message to send."
  "Hello world!")

(provide 'my-message)
;;; my-message.el ends here

1.3. Native and byte compilation

The scripts can be compiled natively with the following Elisp code. The output are eln files in the same directory. Remember that native compilation is a feature of Emacs 28 and newer versions. Also, the eln files are dependant of the Emacs version used to produce them, and they may not work on other (newer or older) versions.

(native-compile "script.el" "script.eln")
(native-compile "my-message.el" "my-message.eln")

The script.eln binary file can be loaded and executed with the following command line:

emacs --batch -Q -l script.eln

An eshell script can be the following.

Welcome to the Emacs shell

~/repos/gemini/cnngimenez.srht.site/site/site $ native-compile script.el script.eln
native-compile my-message.el my-message.eln
emacs --batch -Q -l script.eln

Byte-compliation can be used as well. Its output is portable through different Emacs versions. It is faster than plain Elisp code, however it is slower than native compiled byte code.

The M-x byte-compile-file interactive function, and dired “B” key can be used to byte-compile. Also, the following Elisp code can be used. The output are elc files in the same directory.

(byte-compile-file "my-message.el")
(byte-compile-file "script.el")

To load them use the same command as before:

emacs --batch -Q -l script.elc

The eshell script is:

Welcome to the Emacs shell

~/repos/gemini/cnngimenez.srht.site/site/site $ byte-compile-file script.el
byte-compile-file my-message.el
emacs --batch -Q -l script.elc

2. An echo chatbot

This an echo bot.

2.1. Starting script

#! /usr/bin/env -S emacs -x
;;; script.el --- The chatbot starting script  -*- lexical-binding: t; -*-

;; Copyright 2025 Christian Gimenez
;;
;; Author: Christian Gimenez
;; Maintainer: Christian Gimenez
;; Version: 0.1.0
;; Keywords: comm
;; URL:
;; Package-Requires: ((emacs "27.1"))

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.


;;; Commentary:

;; 

;;; Code:

;; Add emacs-jabber paths
;; (add-to-list 'load-path "/data/data/com.termux/files/home/repos/emacs/emacs-jabber/")
;; (add-to-list 'load-path "/data/data/com.termux/files/home/repos/emacs/emacs-jabber/lisp")
;; (add-to-list 'load-path "/data/data/com.termux/files/home/repos/emacs/emacs-jabber/lisp/jabber-fallback-lib")
(require 'jabber)

;; Import the processing data functions.
;; The following data is required:
;; * `message-make' function that makes the string message to send.
;;
;; Variables/constants expected: `my-message-botname', `my-message-muc-to',
;; `my-message-subject', `my-message-test-to'.
;;
;; The variable `my-message-connection-data' is an alist with the following:
;;   '((username . "USERNAME") (server . "SERVER") (resource . "bot")
;;     (password . "ACCOUNT-PASSWORD"))
(add-to-list 'load-path default-directory)
(require 'my-message)

(defun my-send-message (jc)
  "Send a message.
JC is a current and established jabber connection.

Once connected send and message and do chatbot stuff.

Useful as a hook."
  (message "Sending hello message now.")
  ;; Join to a MUC before sending. Else, the server may answer with error.
  ;; (jabber-muc-join jc my-message-muc-to my-message-botname)

  (sleep-for 2)
  (jabber-send-message jc my-message-test-to "The bot" "Hi!" "chat")
  ;; (jabber-send-message jc my-message-muc-to my-message-subject message "groupchat"))

  (message "Jabber message sent."))

(defun my-echo (jc xml-data)
  (when (my-message-for-me-p xml-data)
    (message "Receiving message...")
    (my-message-process-incomming jc xml-data)))

;; --------------------------------------------------
;; Setup Jabber to only connect and send the message

;; (setq jabber-debug-log-xml "~/xmpp-bot.log") ;; for debugging purposes.

(add-hook 'jabber-message-chain #'my-echo)

;; Maybe should remove MUC auto-join and other hooks? These are the default ones:
;; (setq jabber-post-connect-hooks '(jabber-send-current-presence
;;                                   jabber-muc-autojoin
;;                                   jabber-whitespace-ping-start
;;                                   jabber-vcard-avatars-find-current
;;                                   jabber-enable-carbons))
(add-hook 'jabber-post-connect-hooks #'my-send-message)

(message "Starting connection.")
(jabber-connect (alist-get 'username my-message-connection-data)
                (alist-get 'server my-message-connection-data)
                (alist-get 'resource my-message-connection-data)
                nil ;; registerp
                (alist-get 'password my-message-connection-data))
;; Wait for disconnection
(message "Waiting for message and disconnection.")
(while jabber-connections
  ;; Wait until `jabber-connections' is empty. The `jabber-disconnect-one'
  ;; function deletes the jabber connection from the variable.
  (sleep-for 1))

(provide 'script)
;;; script.el ends here

2.2. The script you should edit

;;; my-message.el --- Message  -*- lexical-binding: t; -*-

;; Copyright 2025 Christian Gimenez
;;
;; Author: Christian Gimenez
;; Maintainer: Christian Gimenez
;; Version: 0.1.0
;; Keywords: comm
;; URL:
;; Package-Requires: ((emacs "27.1"))

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.


;;; Commentary:

;; Make the XMPP message.

;;; Code:
(require 'xml)

(defconst my-message-botname "My bot")
(defconst my-message-muc-to "the-group@muc.server.org")
(defconst my-message-test-to "someone@server.org")
(defconst my-message-subject "The Bot")
(defconst my-message-connection-data '((username . "myusername")
                                (server . "server.org")
                                (resource . "the-bot")
                                (password . "mypassword")))

(defun my-message-xml2sexp (xml-string) ;; For debugging purposes...
  "Convert an XML-STRING into xml-data."
  (car (with-temp-buffer
         (insert xml-string)
         (xml-parse-region))))

(defun my-message-for-me-p (xml-data)
  "Determine if the message is for me.
Check if the origin is from `my-message-test-to' and it is directer to me
according to `my-message-connection-data'."
  (let ((to-jid (jabber-xml-get-attribute xml-data 'to))
        (from-jid (jabber-xml-get-attribute xml-data 'from)))
    (and
     (string= (jabber-jid-username from-jid)
              (jabber-jid-username my-message-test-to))
     (string= (jabber-jid-server from-jid)
              (jabber-jid-server my-message-test-to))
     (string= (jabber-jid-username to-jid)
              (alist-get 'username my-message-connection-data))
     (string= (jabber-jid-server to-jid)
              (alist-get 'server my-message-connection-data)))))

(defun my-message-process-incomming (jc xml-data)
  "Process incomming message.
Emulate this bot is writing and send echo message to its origin.
Ignore any empty message (it colud be another type of message stanza).
JC is the jabber connection.  XML-DATA is an xml-parsed data."
  (let ((jid-from (jabber-xml-get-attribute xml-data 'from))        
        (message (string-trim (or (jabber-xml-path xml-data '(body nil))
                                  "")))
        (my-jid (jabber-connection-jid jc)))

    (unless (string-empty-p message)
      (message "Message from %s: %s" jid-from message)
      ;; Let's simmulate we're writing...
      (jabber-send-sexp jc `(message ((to . ,jid-from)
                                      (from . ,my-jid)
                                      (type . "chat"))
                                     (composing ((xmlns . "http://jabber.org/protocol/chatstates")))
                                     (no-store ((xmlns . "urn:xmpp:hints")))
                                     (no-storage ((xmlns . "urn:xmpp:hints")))))
      (sleep-for 2)
      (jabber-send-message jc jid-from my-message-subject
                           (format "echo: %s" message) "chat")
      ;; Restore chatstate...
      (jabber-send-sexp jc `(message ((to . ,jid-from)
                                      (from . ,my-jid)
                                      (type . "chat"))
                                     (active ((xmlns . "http://jabber.org/protocol/chatstates")))
                                     (no-store ((xmlns . "urn:xmpp:hints")))
                                     (no-storage ((xmlns . "urn:xmpp:hints"))))))))

(provide 'my-message)
;;; my-message.el ends here

3. License

by-sa.png

This work by Christian Gimenez is licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC By-SA 4.0). See more about the license at https://creativecommons.org/licenses/by-sa/4.0/

Reference the original author and original work, even when creating derived works.

Author: Christian Gimenez

Created: 2025-04-13 dom 17:08

Validate