Toggle diff (517 lines)
diff --git a/Makefile.am b/Makefile.am
index d5ec909213..5542aa1c56 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -489,6 +489,7 @@ SCM_TESTS = \
tests/services/file-sharing.scm \
tests/services/configuration.scm \
tests/services/linux.scm \
+ tests/services/telephony.scm \
tests/sets.scm \
tests/size.scm \
tests/status.scm \
diff --git a/doc/guix.texi b/doc/guix.texi
index 2298d512a1..c931cbeb93 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -22526,6 +22526,270 @@ and Error.
@node Telephony Services
@subsection Telephony Services
+@cindex telephony, services
+The @code{(gnu services telephony)} module contains Guix service
+definitions for telephony services. Currently it provides the following
+services:
+
+@subsubheading Jami
+
+@cindex jami, service
+
+This section describes how to configure a Jami server that can be used
+to host video (or audio) conferences, among other uses. The following
+example demonstrates how to specify Jami account archives (backups) to
+be provisioned automatically:
+
+@lisp
+(service jami-service-type
+ (jami-configuration
+ (accounts
+ (list (jami-account
+ (archive "/etc/jami/unencrypted-account-1.gz"))
+ (jami-account
+ (archive "/etc/jami/unencrypted-account-2.gz"))))))
+@end lisp
+
+When the accounts field is specified, the Jami account files of the
+service found under @file{/var/lib/jami} are recreated every time the
+service starts.
+
+Jami accounts and their corresponding backup archives can be generated
+using either the @code{jami-qt} or @code{jami-gnome} Jami clients. The
+accounts should not be password-protected, but it is wise to ensure
+their files are only readable by @samp{root}.
+
+The next example shows how to declare that only some contacts should be
+allowed to communicate with a given account:
+
+@lisp
+(service jami-service-type
+ (jami-configuration
+ (accounts
+ (list (jami-account
+ (archive "/etc/jami/unencrypted-account-1.gz")
+ (peer-discovery? #t)
+ (rendezvous-point? #t)
+ (allowed-contacts
+ '("1dbcb0f5f37324228235564b79f2b9737e9a008f"
+ "2dbcb0f5f37324228235564b79f2b9737e9a008f")))))))
+@end lisp
+
+In this mode, only the declared @code{allowed-contacts} can initiate
+communication with the Jami account. This can be used, for example,
+with rendezvous point accounts to create a private video conferencing
+space.
+
+To put the system administrator in full control of the conferences
+hosted on their system, the Jami service supports the following actions:
+
+@example sh
+herd doc jami list-actions jami
+(list-accounts
+ list-account-details
+ list-banned-contacts
+ list-contacts
+ list-moderators
+ add-moderator
+ ban-contact
+ enable-account
+ disable-account)
+@end example
+
+The above actions aim to provide the most valuable actions for
+moderation purposes, not to cover the whole Jami API. Users wanting to
+interact with the Jami daemon from Guile may be interested in
+experimenting with the @code{(gnu build jami-service)} module, which
+powers the above Shepherd actions.
+
+@c TODO: This should be auto-generated from the doc already defined on
+@c the shepherd-actions themselves in (gnu services telephony).
+The @code{add-moderator} and @code{ban-contact} actions accept a contact
+@emph{fingerprint} (40 characters long hash) as first argument and an
+account fingerprint or username as second argument:
+
+@example sh
+# herd add-moderator jami 1dbcb0f5f37324228235564b79f2b9737e9a008f \
+ f3345f2775ddfe07a4b0d95daea111d15fbc1199
+
+# herd list-moderators jami
+Moderators for account f3345f2775ddfe07a4b0d95daea111d15fbc1199:
+ - 1dbcb0f5f37324228235564b79f2b9737e9a008f
+
+@end example
+
+In the case of @code{ban-contact}, the second username argument is
+optional; when omitted, the account is banned from all Jami accounts:
+
+@example sh
+# herd ban-contact jami 1dbcb0f5f37324228235564b79f2b9737e9a008f
+
+# herd list-banned-contacts jami
+Banned contacts for account f3345f2775ddfe07a4b0d95daea111d15fbc1199:
+ - 1dbcb0f5f37324228235564b79f2b9737e9a008f
+
+@end example
+
+Banned contacts are also stripped from their moderation privileges.
+
+The @code{disable-account} action allows to completely disconnect an
+account from the network, making it unreachable, while
+@code{enable-account} does the inverse. They accept a single account
+username or fingerprint as first argument:
+
+@example sh
+# herd disable-account jami f3345f2775ddfe07a4b0d95daea111d15fbc1199
+
+# herd list-accounts jami
+The following Jami accounts are available:
+ - f3345f2775ddfe07a4b0d95daea111d15fbc1199 (dummy) [disabled]
+
+@end example
+
+The @code{list-account-details} action prints the detailed parameters of
+each accounts in the Recutils format, which means the @command{recsel}
+command can be used to select accounts of interest (@pxref{Selection
+Expressions,,,recutils, GNU recutils manual}). Note that period
+characters (@samp{.}) found in the account parameter keys are mapped to
+underscores (@samp{_}) in the output, to meet the requirements of the
+Recutils format. The following example shows how to print the account
+fingerprints for all accounts operating in the rendezvous point mode:
+
+@example sh
+# herd list-account-details jami | \
+ recsel -p Account.username -e 'Account.rendezVous ~ "true"'
+Account_username: f3345f2775ddfe07a4b0d95daea111d15fbc1199
+@end example
+
+The remaining actions should be self-explanatory.
+
+The complete set of available configuration options is detailed below.
+
+@c TODO: Ideally, the following fragments would be auto-generated at
+@c build time, so that they needn't be manually duplicated.
+@c Auto-generated via (configuration->documentation 'jami-configuration)
+Available @code{jami-configuration} fields are:
+
+@deftypevr {@code{jami-configuration} parameter} package jamid
+The Jami daemon package to use.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} package dbus
+The D-Bus package to use to start the required D-Bus session.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} package nss-certs
+The nss-certs package to use to provide TLS certificates.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} boolean enable-logging?
+Whether to enable logging to syslog.
+
+Defaults to @samp{#t}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} boolean debug?
+Whether to enable debug level messages.
+
+Defaults to @samp{#f}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} boolean auto-answer?
+Whether to force automatic answer to incoming calls.
+
+Defaults to @samp{#f}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} maybe-jami-account-list accounts
+A list of Jami accounts to be (re-)provisioned every time the Jami
+daemon service starts. When providing this field, the account
+directories under @file{/var/lib/jami/} are recreated every time the
+service starts, ensuring a consistent state.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@c Auto-generated via (configuration->documentation 'jami-account)
+Available @code{jami-account} fields are:
+
+@deftypevr {@code{jami-account} parameter} string-or-computed-file archive
+The account archive (backup) file name of the account. This is used to
+provision the account when the service starts. The account archive
+should @emph{not} be encrypted. It is highly recommended to make it
+readable only to the @samp{root} user (i.e., not in the store), to guard
+against leaking the secret key material of the Jami account it contains.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-account-fingerprint-list allowed-contacts
+The list of allowed contacts for the account, entered as their 40
+characters long fingerprint. Messages or calls from accounts not in
+that list will be rejected. When unspecified, the configuration of the
+account archive is used as-is with respect to contacts and public
+inbound calls/messaging allowance, which typically defaults to allow any
+contact to communicate with the account.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-account-fingerprint-list moderators
+The list of contacts that should have moderation privileges (to ban,
+mute, etc. other users) in rendezvous conferences, entered as their 40
+characters long fingerprint. When unspecified, the configuration of the
+account archive is used as-is with respect to moderation, which
+typically defaults to allow anyone to moderate.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-boolean rendezvous-point?
+Whether the account should operate in the rendezvous mode. In this
+mode, all the incoming audio/video calls are mixed into a conference.
+When left unspecified, the value from the account archive prevails.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-boolean peer-discovery?
+Whether peer discovery should be enabled. Peer discovery is used to
+discover other OpenDHT nodes on the local network, which can be useful
+to maintain communication between devices on such network even when the
+connection to the the Internet has been lost. When left unspecified,
+the value from the account archive prevails.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-string-list bootstrap-hostnames
+A list of hostnames or IPs pointing to OpenDHT nodes, that should be
+used to initially join the OpenDHT network. When left unspecified, the
+value from the account archive prevails.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-string name-server-uri
+The URI of the name server to use, that can be used to retrieve the
+account fingerprint for a registered username.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@subsubheading Murmur (VoIP server)
+
@cindex Murmur (VoIP server)
@cindex VoIP server
This section describes how to set up and run a Murmur server. Murmur is
diff --git a/gnu/build/jami-service.scm b/gnu/build/jami-service.scm
new file mode 100644
index 0000000000..bb43b578f9
--- /dev/null
+++ b/gnu/build/jami-service.scm
@@ -0,0 +1,587 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix 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.
+;;;
+;;; GNU Guix 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 GNU Guix. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;;
+;;; This module contains helpers used as part of the jami-service-type
+;;; definition.
+;;;
+;;; Code:
+
+(define-module (gnu build jami-service)
+ #:use-module (ice-9 format)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 peg)
+ #:use-module (ice-9 rdelim)
+ #:use-module (ice-9 regex)
+ #:use-module (rnrs io ports)
+ #:autoload (shepherd service) (fork+exec-command)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-26)
+ #:export (account-fingerprint?
+ account-details->recutil
+ get-accounts
+ get-usernames
+ set-account-details
+ add-account
+ account->username
+ username->account
+ username->contacts
+ enable-account
+ disable-account
+
+ add-contact
+ remove-contact
+
+ set-all-moderators
+ set-moderator
+ username->all-moderators?
+ username->moderators
+
+ dbus-available-services
+ dbus-service-available?
+
+ %send-dbus-binary
+ %send-dbus-bus
+ %send-dbus-user
+ %send-dbus-group
+ %send-dbus-debug
+ send-dbus
+
+ with-retries))
+
+;;;
+;;; Utilities.
+;;;
+
+(define-syntax-rule (with-retries n delay body ...)
+ "Retry the code in BODY up to N times until it doesn't raise an exception
+nor return #f, else raise an error. A delay of DELAY seconds is inserted
+before each retry."
+ (let loop ((attempts 0))
+ (catch #t
+ (lambda ()
+ (let ((result (begin body ...)))
+ (if (not result)
+ (error "failed attempt" attempts)
+ result)))
+ (lambda args
+ (if (< attempts n)
+ (begin
+ (sleep delay) ;else wait and retry
+ (loop (+ 1 attempts)))
+ (error "maximum number of retry attempts reached"
+ body ... args))))))
+
+(define (alist->list alist)
+ "Flatten ALIST into a list."
+ (append-map (match-lambda
+ (() '())
+ ((key . value)
+ (list key value)))
+ alist))
+
+(define account-fingerprint-rx (make-regexp "[0-9A-f]{40}"))
+
+(define (account-fingerprint? val)
+ "A Jami account fingerprint is 40 characters long and only contains
+hexadecimal characters."
+ (and (string? val)
+ (regexp-exec account-fingerprint-rx val)))
+
+
+;;;
+;;; D-Bus reply parser.
+;;;
+
+(define (parse-dbus-reply reply)
+ "Return the parse tree of REPLY, a string returned by the 'dbus-send'
+command."
+ ;; Refer to 'man 1 dbus-send' for the grammar reference. Note that the
+ ;; format of the replies doesn't match the format of the input, which is the
+ ;; one documented, but it gives an idea. For an even better reference, see
+ ;; the `print_iter' procedure of the 'dbus-print-message.c' file from the
+ ;; 'dbus' package sources.
+ (define-peg-string-patterns
+ "contents <- header (item / container (item / container*)?)
+ item <-- WS type WS value NL
+ container <- array / dict / variant
+ array <-- array-start (item / container)* array-end
+ dict <-- array-start dict-entry* array-end
+ dict-entry <-- dict-entry-start item item dict-entry-end
+ variant <-- variant-start item
+ type <-- 'string' / 'int16' / 'uint16' / 'int32' / 'uint32' / 'int64' /
+ 'uint64' / 'double' / 'byte' / 'boolean' / 'objpath'
+ value <-- (!NL .)* NL
+ header < (!NL .)* NL
+ variant-start < WS 'variant'
+ array-start < WS 'array [' NL
+ array-end < WS ']' NL
+ dict-entry-start < WS 'dict entry(' NL
+ dict-entry-end < WS ')' NL
+ DQ < '\"'
+ WS < ' '*
+ NL < '\n'*")
+
+ (peg:tree (match-pattern contents reply)))
+
+(define (strip-quotes text)
+ "Strip the leading and trailing double quotes (\") characters from TEXT."
+ (let* ((text* (if (string-prefix? "\"" text)
+ (string-drop text 1)
+ text))
+ (text** (if (string-suffix? "\"" text*)
+ (string-drop-right text* 1)
+ text*)))
+ text**))
+
+(define (deserialize-item item)
+ "Return the value described by the ITEM parse tree as a Guile object."
+ ;; Strings are printed wrapped in double quotes (see the print_iter
+ ;; procedure in dbus-print-message.c).
+ (match item
+ (('item ('type "string") ('value value))
+ (strip-quotes value))
+ (('item ('type "boolean") ('value value))
+ (if (string=? "true" value)
+ #t
+ #f))
+ (('item _ ('value value))
+ value)))
+
+(define (serialize-boolean bool)
+ "Return the serialized format expected by dbus-send for BOOL."
+ (format #f "boolean:~:[false~;true~]" bool))
+
+(define (dict->alist dict-parse-tree)
+ "Translate a dict parse tree to an alist."
+ (define (tuples->alist tuples)
+ (map (lambda (x) (apply cons x)) tuples))
+
+ (match dict-parse-tree
+ ('dict
+ '())
+ (('dict ('dict-entry keys values) ...)
+ (let ((keys* (map deserialize-item keys))
+ (values* (map deserialize-item values)))
+ (tuples->alist (zip keys* values*))))))
+
+(define (array->list array-parse-tree)
+ "Translate an array parse tree to a list."
+ (match array-parse-tree
+ ('array
+ '())
+ (('array items ...)
+ (map deserialize-item items))))
+
+
+;;;
+;;; Low-level, D-Bus-related procedures.
+;;;
+
+;;; The following parameters are used in the jami-service-type service
+;;; definition to conveniently customize the behavior of the send-dbus helper,
+;;; even when called indirectly.
+(define %send-dbus-binary (make-parameter "dbus-send"))
+(define %send-dbus-bus (make-parameter #f))
+(define %send-dbus-user (make-parameter #f))
+(define %send-dbus-group (make-parameter #f))
+(define %send-dbus-debug (make-parameter #f))
+
+(define* (send-dbus #:key service path interface method
+ bus
+ dbus-send
+ user group
+ timeout
+ arguments)
+ "Return the response of DBUS-SEND, else raise an error. Unless explicitly
+provided, DBUS-SEND takes the value of the %SEND-DBUS-BINARY parameter. BUS
+can be used to specify the bus address, such as 'unix:path=/var/run/jami/bus'.
+Alternatively, the %SEND-DBUS-BUS parameter can be used. ARGUMENTS can be
+used to pass input values to a D-Bus method call. TIMEOUT is the amount of
+time to wait for a reply in milliseconds before giving up with an error. USER
+and GROUP allow choosing under which user/group the DBUS-SEND command is
+executed. Alternatively, the %SEND-DBUS-USER and %SEND-DBUS-GROUP parameters
+can be used instead."
+ (let* ((command `(,@(if dbus-send
+ (list dbus-send)
+ (list (%send-dbus-binary)))
+ ,@(if (or bus (%send-dbus-