Compare commits


15 Commits

Author SHA1 Message Date
Sebastian Crane b1079768ee Set version in build system to 1.1.1-SNAPSHOT
Signed-off-by: Sebastian Crane <>
2022-05-14 14:03:59 +01:00
Sebastian Crane 9ed4b34083 Release version 1.1.0
Signed-off-by: Sebastian Crane <>
2022-05-14 13:36:31 +01:00
Sebastian Crane 0a939da401 Document the release process in
This commit adds documentation in the file about how
releases of matchbot are made and published on Maven Central, and how
the version number changes between releases are determined.

Signed-off-by: Sebastian Crane <>
2022-04-24 18:52:39 +01:00
Sebastian Crane 93306d6d34 Document the build system in
This commit adds documentation in the file about how to
generate POM, JAR and uberjar files using matchbot's build system.

Signed-off-by: Sebastian Crane <>
2022-04-24 18:48:30 +01:00
Sebastian Crane bfd321cdb3 Add build system
This commit adds support for generating POM files and builds of matchbot
as JAR or uberjar (standalone JAR) files.

Resolves issue #3

Signed-off-by: Sebastian Crane <>
2022-04-09 21:48:42 +01:00
Sebastian Crane 69e154ea87 Add :build alias with relevant dependencies
This commit creates a new alias :build in deps.edn, adding the and tools-pom libraries as dependencies. These are needed
for generating proper JAR and POM files with a Clojure build system.

Signed-off-by: Sebastian Crane <>
2022-04-09 21:48:42 +01:00
Sebastian Crane 7fc472c269 Remove superfluous utilisation of 'do' forms
Since defn evaluates the expressions it its body in the same way as do,
this commit removes the superfluous uses of do where it appears directly
as a function body.

Signed-off-by: Sebastian Crane <>
2022-04-09 22:46:18 +02:00
Sebastian Crane 428c9318fd Set default test runner function for :test alias
This commit sets in deps.edn the default main function to be run when
using the :test alias, making the command to run tests shorter, and
documents this shorter command in

Signed-off-by: Sebastian Crane <>
2022-04-02 18:35:04 +01:00
Sebastian Crane b099d6bfe1 Release version 1.0.1
Signed-off-by: Sebastian Crane <>
2022-04-01 15:52:51 +01:00
Sebastian Crane 6a594c5ad3 Use strings instead of keywords for game names
This commit changes the internal representation of game names in the
state data to be strings instead of keywords. This fixes a potential
memory leak relating to games that no longer have any players, since
strings, unlike keywords, are garbage collected when no longer in use.

Resolves issue #2

Signed-off-by: Sebastian Crane <>
2022-03-30 12:11:10 +01:00
Sebastian Crane 6cce43f137 Move processing out of call
This commit removes from the call to the
conversion of the serialised list of players into a Clojure set, and
adds an independent function to do this task instead.

Since the structure of the serialised data is known in advance, the
recursive characteristics of are not needed.

Resolves issue #4

Signed-off-by: Sebastian Crane <>
2022-03-28 21:52:52 +01:00
Sebastian Crane 91cbdb69da Simplify the system start and restart functions
This commit changes the binding name of system/start's argument to
_ (since it is currently not used within the function), and simplifies
the system/restart function by not swapping the system Var's content
with nil before filling it with the new-created system state.

Eventually, these changes will be reverted when the initialisation of
the system is separated from the starting of that system, which will
mean that system/start will read its argument and that system/restart
will have to reinitialise the system state before restarting it.

Signed-off-by: Sebastian Crane <>
2022-03-24 21:51:06 +00:00
Sebastian Crane fecd09c656 Remove unnecessary system/system function
A nil value acts as an empty map wherever a map value is expected. For
this reason, this commit removes the system/system function (which
returns an empty map), since a call to it can safely be replaced with a
nil value.

Signed-off-by: Sebastian Crane <>
2022-03-22 22:51:59 +00:00
Sebastian Crane 0abd793fe7 Use a Var instead of an Atom for system state
The Atom's swap! function may run its provided function multiple
times. Because of this, using an Atom for the system state can
occasionally result in duplicate TCP connections being opened when the
system is started. These connections remain open until they
timeout (this is typically 5 minutes for IRC).

This commit changes the container for the system state from an Atom to a
Var, whose alter-var-root function (analogous to swap!) does not have
this issue.

Signed-off-by: Sebastian Crane <>
2022-03-22 22:47:00 +00:00
Sebastian Crane 56094478db Remove use of state container in main function
Since the main function will only ever start and stop one system, this
commit simplifies the main function to use a single lexical binding for
the system state rather than creating an Atom.

Signed-off-by: Sebastian Crane <>
2022-03-22 22:38:30 +00:00
9 changed files with 191 additions and 66 deletions

View File

@ -5,8 +5,24 @@
`matchbot` uses [version 2.0.0 of the Semantic Versioning]( scheme.
## [1.1.0] - 2022-05-14
* Add build system for generating POM, JAR and uberjar (standalone JAR) files
* Improve code quality
## [1.0.1] - 2022-04-01
* Fix vulnerability that causes serialised data to be deleted when it contains certain user input
* Change internal representation of game names, fixing a potential memory leak for long-running instances
* Improve reliability and code quality of the startup and shutdown sequences
## [1.0.0] - 2022-03-03
* Initial release

View File

@ -40,3 +40,37 @@ For example:
> `;; SPDX-FileCopyrightText: 2022 Joe Bloggs <>`
If the copyright to your contributions is held by your employer, put your employer's name in brackets after your own name.
## Build system
### Building a POM file
The POM file lists the dependencies needed to run `matchbot` as well as some additional information that can help people learn more about `matchbot`.
To generate a POM file, run `clojure -T:build pom`; you should find the generated `POM.xml` file in the `target/` directory.
### Building a JAR file
A JAR file contains all the source code of `matchbot` in a form that the JVM can load and pass to the Clojure compiler to run.
To generate a JAR file, run `clojure -T:build jar`; again, you should find the JAR file called something like `matchbot-x.x.x.jar` in the `target/` directory.
### Building an uberjar
An uberjar is much like a normal JAR file, but comes with all the dependencies of `matchbot` bundled in it.
This means that it can run directly on the JVM without Clojure being installed (it contains a copy of the Clojure compiler itself).
To generate an uberjar, run `clojure -T:build uber`; you should find the uberjar called something like `matchbot-x.x.x-standalone.jar` in the `target/` directory.
Please note that if you distribute an uberjar, you must not only comply with the licence of `matchbot`, but also the licences of all `matchbot`'s dependencies, both transitive and intransitive.
## Versioning and release process
### Semantic Versioning
`matchbot` uses [version 2.0.0 of the Semantic Versioning]( scheme, but there is still ambiguity in what exactly comprises the 'Public API' (used for determining the right part of the version to increment) for something like `matchbot`.
This 'Public API' is defined for `matchbot` as everything that is accessible by the end user or administrator of a `matchbot` instance.
For example, a change that requires the configuration file to be updated warrants a major version increment because it effects the administrator; however, a change to the structure of the internal namespaces would only require a patch level version increment because it doesn't affect either the administrator or the end user.
### Release process
At some point after a new feature has been added to `matchbot` or a bug has been fixed, a release will be made.
Once a suitable version increment for the type of changes has been determined, the `` file at the root of the repository will be updated with release notes documenting the changes made in that version.
Then, a JAR file and a POM file will be produced using the build system (see above for more information), signed using GPG and finally uploaded to [Maven Central](
Currently this process is done by [Sebastian Crane](; if you would like to help with making releases, please familiarise yourself with the process (you can try everything locally except upload to Maven Central) and get in contact! 😀

View File

@ -55,7 +55,7 @@ clojure -M -m system
Running the tests is a similar process to running the main application:
clojure -M:test -m kaocha.runner
clojure -M:test
### Starting a development REPL
@ -66,18 +66,18 @@ Since `matchbot` uses Clojure's [tools.deps library](
You can create, start and stop an instance of the chatbot process with the functions in the `system` namespace:
``` clojure
;; creating a new instance
(def my-instance (atom (system/system)))
;; creating a new instance - an empty Var
(def my-instance nil)
;; starting the instance
(swap! my-instance system/start)
(alter-var-root #'my-instance system/start)
;; restarting the instance
(system/restart my-instance)
(system/restart #'my-instance)
;; stopping and resetting the instance
(swap! my-instance stop)
(reset! my-instance (system/system))
(alter-var-root #'my-instance system/stop)
(alter-var-root #'my-instance (constantly nil))
Once you are familiar with nREPL, you can additionally use [tools.namespace.repl]( to make reevaluating (reloading) your changes easier:

build.clj Normal file
View File

@ -0,0 +1,77 @@
;; SPDX-License-Identifier: Apache-2.0
;; SPDX-FileCopyrightText: 2022 Sebastian Crane <>
(ns build
(:require [ :as b]
[tools-pom.tasks :as pom]))
(def application 'org.libregaming/matchbot)
(def version "1.1.1-SNAPSHOT")
(def src-dirs ["src"])
(def target-dir "target")
(def class-dir (format "%s/%s" target-dir "classes"))
(def basis (b/create-basis {:project "deps.edn"}))
(def pom-file (format "%s/pom.xml" target-dir))
(def jar-file (format "%s/%s-%s.jar" target-dir (name application) version))
(def uber-file (format "%s/%s-%s-standalone.jar" target-dir (name application) version))
(defn clean [_]
(b/delete {:path target-dir}))
(defn uber [_]
(b/delete {:path class-dir})
(b/copy-dir {:src-dirs src-dirs
:target-dir class-dir})
(b/compile-clj {:basis basis
:src-dirs src-dirs
:class-dir class-dir})
(b/uber {:class-dir class-dir
:uber-file uber-file
:basis basis
:main 'system}))
(defn jar [_]
(b/delete {:path class-dir})
(b/copy-dir {:src-dirs src-dirs
:target-dir class-dir})
(b/jar {:class-dir class-dir
:jar-file jar-file}))
(defn pom [_]
{:lib application
:version version
:write-pom true
:validate-pom true
"A chatbot for announcing upcoming matches and finding fellow players, written for the LibreGaming community"
{:name "Apache-2.0"
:url ""}]
{:id "seabass"
:name "Sebastian Crane"
:email ""
:organization "LibreGaming"
:organization-url ""
:roles [:role "Maintainer"]
:timezone "Europe/London"}]
{:url ""
:connection "scm:git:"
:developer-connection "scm:git:ssh://"}
{:system "Gitea"
:url ""}}})
(b/copy-file {:src "pom.xml" :target pom-file})
(b/delete {:path "pom.xml"}))
(defn all [_]
(jar nil)
(uber nil)
(pom nil))

View File

@ -6,4 +6,8 @@
clj-commons/clj-yaml {:mvn/version "0.7.107"}
irclj/irclj {:mvn/version "0.5.0-alpha4"}}
:aliases {:test {:extra-paths ["test"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.60.972"}}}}}
:extra-deps {lambdaisland/kaocha {:mvn/version "1.60.972"}}
:main-opts ["-m" "kaocha.runner"]}
:build {:deps {io.github.clojure/ {:git/tag "v0.8.1" :git/sha "7d40500"}
com.github.pmonks/tools-pom {:mvn/version "1.0.74"}}
:ns-default build}}}

View File

@ -6,15 +6,15 @@
(defn keywordise-game [game]
(defn lower-case-game [game]
(when (string? game)
(keyword (str/lower-case game))))
(str/lower-case game)))
(defn sort-case-insensitive [coll]
(sort #(apply compare (map str/lower-case %&)) coll))
(defn match-string [& {:keys [state game player]}]
(as-> (keywordise-game game) x
(as-> (lower-case-game game) x
(game/get-players-of-game state x)
(disj x player)
(sort-case-insensitive x)
@ -22,7 +22,7 @@
(str "Anyone ready for " game "? " x)))
(defn list-players-string [& {:keys [state game]}]
(as-> (keywordise-game game) x
(as-> (lower-case-game game) x
(game/get-players-of-game state x)
(sort-case-insensitive x)
(map #(str " _" % "_") x)
@ -46,7 +46,7 @@
(str "Games with a list of players: "
", "
(sort-case-insensitive (map name (game/get-games state))))))
(sort-case-insensitive (game/get-games state)))))
(defn help-string [& {:keys []}]
" !list - show all the games that have a list of players
@ -59,7 +59,7 @@
(let [message-parts (str/split message #"\s")
command (if-let [x (first message-parts)] (str/lower-case x) "")
game (second message-parts)
game-keyword (keywordise-game game)]
game-keyword (lower-case-game game)]
{:command command
:game game
:game-keyword game-keyword}))

View File

@ -4,23 +4,26 @@
(ns system
(:require [irc]
[ :as json]
[clj-yaml.core :as yaml]))
[clojure.set :as set]
[clj-yaml.core :as yaml])
(defn json-data-reader [key value]
(if (= key :games)
(into (empty value)
(map #(hash-map (first %)
(set (second %)))
(defn setify-vals [x]
(reduce #(assoc %1
(first %2)
(set (second %2)))
{} x))
(defn process-json [x]
(-> (set/rename-keys x {"games" :games})
(update :games setify-vals)))
(defn load-state [f]
(with-open [datafile ( f)]
(json/read datafile
:value-fn json-data-reader
:key-fn keyword))
(catch Exception e nil)))
(with-open [datafile ( f)]
(json/read datafile))
(catch Exception e nil))))
(defn save-state [f data]
@ -34,12 +37,7 @@
(yaml/parse-stream datafile))
(catch Exception e nil)))
(defn system []
{:config nil
:state nil
:irc nil})
(defn start [system]
(defn start [_]
(let [config (load-config "config.yaml")
state (atom (load-state (:data-file config)))
irc (irc/new-irc-connection state config)]
@ -49,20 +47,16 @@
:irc irc}))
(defn stop [system]
(get-in system [:config :data-file])
(deref (:state system)))
(irclj.core/quit (system :irc))))
(get-in system [:config :data-file])
(deref (:state system)))
(irclj.core/quit (system :irc)))
(defn restart [system-atom]
(swap! system-atom stop)
(reset! system-atom (system))
(swap! system-atom start)))
(defn restart [system-var]
(stop (deref system-var))
(alter-var-root system-var start))
(defn -main [& args]
(let [main-system (atom (system/system))]
(swap! main-system system/start)
(let [main-system (system/start nil)]
(.addShutdownHook (Runtime/getRuntime)
(Thread. (partial swap! main-system stop)))))
(Thread. #(stop main-system)))))

View File

@ -5,13 +5,13 @@
(:require [clojure.test :refer :all]
[bot :refer :all]))
(def test-state '{:games {:hypothetical-shooter #{"abc" "xyz" "123"}
:quasi-rts #{"abc" "123"}
:imaginary-rpg #{"xyz" "abc"}}})
(def test-state '{:games {"hypothetical-shooter" #{"abc" "xyz" "123"}
"quasi-rts" #{"abc" "123"}
"imaginary-rpg" #{"xyz" "abc"}}})
(deftest keywordise-game-test
(is (= :quasi-rts
(keywordise-game "Quasi-RTS"))))
(deftest lower-case-game-test
(is (= "quasi-rts"
(lower-case-game "Quasi-RTS"))))
(deftest sort-case-insensitive-test
(is (= ["A" "b" "C"]
@ -38,7 +38,7 @@
(list-games-string :state test-state))))
(deftest split-message-test
(is (= {:command "!match" :game "Quasi-Rts" :game-keyword :quasi-rts}
(is (= {:command "!match" :game "Quasi-Rts" :game-keyword "quasi-rts"}
(split-message "!match Quasi-Rts "))))
(deftest dispatch-command-test
@ -46,7 +46,7 @@
(is (and
(= "Added 123 to the list of players for Imaginary-RPG!"
(dispatch-command state-atom "123" "!add Imaginary-RPG"))
(= {:games {:hypothetical-shooter #{"abc" "xyz" "123"}
:quasi-rts #{"abc" "123"}
:imaginary-rpg #{"xyz" "abc" "123"}}}
(= {:games {"hypothetical-shooter" #{"abc" "xyz" "123"}
"quasi-rts" #{"abc" "123"}
"imaginary-rpg" #{"xyz" "abc" "123"}}}

View File

@ -5,31 +5,31 @@
(:require [clojure.test :refer :all]
[game :refer :all]))
(def test-state '{:games {:hypothetical-shooter #{"player-one" "player-two" "player-three"}
:quasi-rts #{"player-two" "player-four"}
:imaginary-rpg #{"player-one" "player-three" "player-four"}}})
(def test-state '{:games {"hypothetical-shooter" #{"player-one" "player-two" "player-three"}
"quasi-rts" #{"player-two" "player-four"}
"imaginary-rpg" #{"player-one" "player-three" "player-four"}}})
(deftest get-players-of-game-test
(is (=
'#{"player-two" "player-four"}
(get-players-of-game test-state :quasi-rts))))
(get-players-of-game test-state "quasi-rts"))))
(deftest add-player-of-game-test
(is (=
'#{"player-one" "player-two" "player-four"}
(get-in (add-player-of-game test-state :quasi-rts "player-one") [:games :quasi-rts]))))
(get-in (add-player-of-game test-state "quasi-rts" "player-one") [:games "quasi-rts"]))))
(deftest remove-player-of-game-test
(is (=
'#{"player-one" "player-three"}
(get-in (remove-player-of-game test-state :imaginary-rpg "player-four") [:games :imaginary-rpg]))))
(get-in (remove-player-of-game test-state "imaginary-rpg" "player-four") [:games "imaginary-rpg"]))))
(deftest get-games-test
(is (=
'#{:hypothetical-shooter :quasi-rts :imaginary-rpg}
'#{"hypothetical-shooter" "quasi-rts" "imaginary-rpg"}
(set (get-games test-state)))))
(deftest remove-game-test
(is (=
'#{:hypothetical-shooter :imaginary-rpg}
(set (keys (:games (remove-game test-state :quasi-rts)))))))
'#{"hypothetical-shooter" "imaginary-rpg"}
(set (keys (:games (remove-game test-state "quasi-rts")))))))