123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2021, 2022 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 (gnu build dbus-service)
- #:use-module (ice-9 format)
- #:use-module (ice-9 match)
- #:use-module (ice-9 rdelim)
- #:use-module (ice-9 regex)
- #:use-module (srfi srfi-1)
- #:use-module (srfi srfi-26)
- #:export (jami-service-available?
- 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))
- ;;;
- ;;; Utilities.
- ;;;
- (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-Fa-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)))
- (define (validate-fingerprint fingerprint)
- "Validate that fingerprint is 40 characters long."
- (unless (account-fingerprint? fingerprint)
- (error "Account fingerprint is not valid:" fingerprint)))
- (define (jami-service-available?)
- "Whether the Jami D-Bus service was acquired by the D-Bus daemon."
- (unless (%current-dbus-connection)
- (initialize-dbus-connection!))
- (dbus-service-available? "cx.ring.Ring"))
- ;;;
- ;;; Bindings for the Jami D-Bus API.
- ;;;
- (define* (call-configuration-manager-method method #:optional arguments
- #:key timeout)
- "Query the Jami D-Bus ConfigurationManager interface with METHOD applied to
- ARGUMENTS. TIMEOUT can optionally be provided as a value in seconds."
- (unless (%current-dbus-connection)
- (initialize-dbus-connection!))
- (call-dbus-method method
- #:path "/cx/ring/Ring/ConfigurationManager"
- #:destination "cx.ring.Ring"
- #:interface "cx.ring.Ring.ConfigurationManager"
- #:arguments arguments
- #:timeout timeout))
- ;;; The following methods are for internal use; they make use of the account
- ;;; ID, an implementation detail of Jami the user should not need to be
- ;;; concerned with.
- (define (get-account-ids)
- "Return the available Jami account identifiers (IDs). Account IDs are an
- implementation detail used to identify the accounts in Jami."
- (vector->list (call-configuration-manager-method "getAccountList")))
- (define (id->account-details id)
- "Retrieve the account data associated with the given account ID."
- (vector->list (call-configuration-manager-method "getAccountDetails"
- (list id))))
- (define (id->volatile-account-details id)
- "Retrieve the account data associated with the given account ID."
- (vector->list (call-configuration-manager-method "getVolatileAccountDetails"
- (list id))))
- (define (id->account id)
- "Retrieve the complete account data associated with the given account ID."
- (append (id->volatile-account-details id)
- (id->account-details id)))
- (define %username-to-id-cache #f)
- (define (invalidate-username-to-id-cache!)
- (set! %username-to-id-cache #f))
- (define (username->id username)
- "Return the first account ID corresponding to USERNAME."
- (unless (assoc-ref %username-to-id-cache username)
- (set! %username-to-id-cache
- (append-map
- (lambda (id)
- (let* ((account (id->account id))
- (username (assoc-ref account "Account.username"))
- (registered-name (assoc-ref account
- "Account.registeredName")))
- `(,@(if username
- (list (cons username id))
- '())
- ,@(if registered-name
- (list (cons registered-name id))
- '()))))
- (get-account-ids))))
- (or (assoc-ref %username-to-id-cache username)
- (let ((message (format #f "no account ID for ~:[username~;fingerprint~]"
- (account-fingerprint? username))))
- (error message username))))
- (define (account->username account)
- "Return the public key fingerprint of ACCOUNT."
- (assoc-ref account "Account.username"))
- (define (id->username id)
- "Return the public key fingerprint corresponding to account with ID, else #f."
- (account->username (id->account id)))
- (define (get-accounts)
- "Return the list of all accounts, as a list of alists."
- (map id->account (get-account-ids)))
- (define (get-usernames)
- "Return the list of the usernames associated with the present accounts."
- (map account->username (get-accounts)))
- (define (username->account username)
- "Return the first account associated with USERNAME, else #f.
- USERNAME can be either the account 40 characters public key fingerprint or a
- registered username."
- (find (lambda (account)
- (member username
- (list (assoc-ref account "Account.username")
- (assoc-ref account "Account.registeredName"))))
- (get-accounts)))
- (define (add-account archive)
- "Import the Jami account ARCHIVE and return its account ID. The archive
- should *not* be encrypted with a password. Return the username associated
- with the account."
- (invalidate-username-to-id-cache!)
- (let ((id (call-configuration-manager-method
- "addAccount" (list `#(("Account.archivePath" . ,archive)
- ("Account.type" . "RING"))))))
- ;; The account information takes some time to be populated.
- (with-retries 20 1
- (let ((username (id->username id)))
- (if (and=> username (negate string-null?))
- username
- #f)))))
- (define (remove-account username)
- "Delete the Jami account associated with USERNAME, the account 40 characters
- fingerprint or a registered username."
- (let ((id (username->id username)))
- (call-configuration-manager-method "removeAccount" (list id)))
- (invalidate-username-to-id-cache!))
- (define* (username->contacts username)
- "Return the contacts associated with the account of USERNAME as two values;
- the first one being the regular contacts and the second one the banned
- contacts. USERNAME can be either the account 40 characters public key
- fingerprint or a registered username. The contacts returned are represented
- using their 40 characters fingerprint."
- (let* ((id (username->id username))
- ;; The contacts are returned as "aa{ss}", that is, an array of arrays
- ;; containing (string . string) pairs.
- (contacts (map vector->list
- (vector->list (call-configuration-manager-method
- "getContacts" (list id)))))
- (banned? (lambda (contact)
- (and=> (assoc-ref contact "banned")
- (cut string=? "true" <>))))
- (banned (filter banned? contacts))
- (not-banned (filter (negate banned?) contacts))
- (fingerprint (cut assoc-ref <> "id")))
- (values (map fingerprint not-banned)
- (map fingerprint banned))))
- (define* (remove-contact contact username #:key ban?)
- "Remove CONTACT, the 40 characters public key fingerprint of a contact, from
- the account associated with USERNAME (either a fingerprint or a registered
- username). When BAN? is true, also mark the contact as banned."
- (validate-fingerprint contact)
- (let ((id (username->id username)))
- (call-configuration-manager-method "removeContact" (list id contact ban?))))
- (define (add-contact contact username)
- "Add CONTACT, the 40 characters public key fingerprint of a contact, to the
- account of USERNAME (either a fingerprint or a registered username)."
- (validate-fingerprint contact)
- (let ((id (username->id username)))
- (call-configuration-manager-method "addContact" (list id contact))))
- (define* (set-account-details details username #:key timeout)
- "Set DETAILS, an alist containing the key value pairs to set for the account
- of USERNAME, a registered username or account fingerprint. The value of the
- parameters not provided are unchanged. TIMEOUT is a value in milliseconds to
- pass to the `call-configuration-manager-method' procedure."
- (let* ((id (username->id username))
- (current-details (id->account-details id))
- (updated-details (map (match-lambda
- ((key . value)
- (or (and=> (assoc-ref details key)
- (cut cons key <>))
- (cons key value))))
- current-details)))
- (call-configuration-manager-method
- "setAccountDetails" (list id (list->vector updated-details))
- #:timeout timeout)))
- (define (set-all-moderators enabled? username)
- "Set the 'AllModerators' property to enabled? for the account of USERNAME, a
- registered username or account fingerprint."
- (let ((id (username->id username)))
- (call-configuration-manager-method "setAllModerators" (list id enabled?))))
- (define (username->all-moderators? username)
- "Return the 'AllModerators' property for the account of USERNAME, a
- registered username or account fingerprint."
- (let ((id (username->id username)))
- (call-configuration-manager-method "isAllModerators" (list id))))
- (define (username->moderators username)
- "Return the moderators for the account of USERNAME, a registered username or
- account fingerprint."
- (let* ((id (username->id username)))
- (vector->list (call-configuration-manager-method "getDefaultModerators"
- (list id)))))
- (define (set-moderator contact enabled? username)
- "Set the moderator flag to ENABLED? for CONTACT, the 40 characters public
- key fingerprint of a contact for the account of USERNAME, a registered
- username or account fingerprint."
- (validate-fingerprint contact)
- (let* ((id (username->id username)))
- (call-configuration-manager-method "setDefaultModerator"
- (list id contact enabled?))))
- (define (disable-account username)
- "Disable the account known by USERNAME, a registered username or account
- fingerprint."
- (set-account-details '(("Account.enable" . "false")) username
- ;; Waiting for the reply on this command takes a very
- ;; long time that trips the default D-Bus timeout value
- ;; (25 s), for some reason.
- #:timeout 60))
- (define (enable-account username)
- "Enable the account known by USERNAME, a registered username or account
- fingerprint."
- (set-account-details '(("Account.enable" . "true")) username))
- ;;;
- ;;; Presentation procedures.
- ;;;
- (define (.->_ text)
- "Map each period character to underscore characters."
- (string-map (match-lambda
- (#\. #\_)
- (c c))
- text))
- (define (account-details->recutil account-details)
- "Serialize the account-details alist into a recutil string. Period
- characters in the keys are normalized to underscore to meet Recutils' format
- requirements."
- (define (pair->recutil-property pair)
- (match pair
- ((key . value)
- (string-append (.->_ key) ": " value))))
- (define sorted-account-details
- ;; Have the account username, display name and alias appear first, for
- ;; convenience.
- (let ((first-items '("Account.username"
- "Account.displayName"
- "Account.alias")))
- (append (map (cut assoc <> account-details) first-items)
- (fold alist-delete account-details first-items))))
- (string-join (map pair->recutil-property sorted-account-details) "\n"))
|