jami-service.scm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2021, 2022 Maxim Cournoyer <maxim.cournoyer@gmail.com>
  3. ;;;
  4. ;;; This file is part of GNU Guix.
  5. ;;;
  6. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  7. ;;; under the terms of the GNU General Public License as published by
  8. ;;; the Free Software Foundation; either version 3 of the License, or (at
  9. ;;; your option) any later version.
  10. ;;;
  11. ;;; GNU Guix is distributed in the hope that it will be useful, but
  12. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;;; GNU General Public License for more details.
  15. ;;;
  16. ;;; You should have received a copy of the GNU General Public License
  17. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  18. ;;; Commentary:
  19. ;;;
  20. ;;; This module contains helpers used as part of the jami-service-type
  21. ;;; definition.
  22. ;;;
  23. ;;; Code:
  24. (define-module (gnu build jami-service)
  25. #:use-module (gnu build dbus-service)
  26. #:use-module (ice-9 format)
  27. #:use-module (ice-9 match)
  28. #:use-module (ice-9 rdelim)
  29. #:use-module (ice-9 regex)
  30. #:use-module (srfi srfi-1)
  31. #:use-module (srfi srfi-26)
  32. #:export (jami-service-available?
  33. account-fingerprint?
  34. account-details->recutil
  35. get-accounts
  36. get-usernames
  37. set-account-details
  38. add-account
  39. account->username
  40. username->account
  41. username->contacts
  42. enable-account
  43. disable-account
  44. add-contact
  45. remove-contact
  46. set-all-moderators
  47. set-moderator
  48. username->all-moderators?
  49. username->moderators))
  50. ;;;
  51. ;;; Utilities.
  52. ;;;
  53. (define (alist->list alist)
  54. "Flatten ALIST into a list."
  55. (append-map (match-lambda
  56. (() '())
  57. ((key . value)
  58. (list key value)))
  59. alist))
  60. (define account-fingerprint-rx (make-regexp "[0-9A-Fa-f]{40}"))
  61. (define (account-fingerprint? val)
  62. "A Jami account fingerprint is 40 characters long and only contains
  63. hexadecimal characters."
  64. (and (string? val)
  65. (regexp-exec account-fingerprint-rx val)))
  66. (define (validate-fingerprint fingerprint)
  67. "Validate that fingerprint is 40 characters long."
  68. (unless (account-fingerprint? fingerprint)
  69. (error "Account fingerprint is not valid:" fingerprint)))
  70. (define (jami-service-available?)
  71. "Whether the Jami D-Bus service was acquired by the D-Bus daemon."
  72. (unless (%current-dbus-connection)
  73. (initialize-dbus-connection!))
  74. (dbus-service-available? "cx.ring.Ring"))
  75. ;;;
  76. ;;; Bindings for the Jami D-Bus API.
  77. ;;;
  78. (define* (call-configuration-manager-method method #:optional arguments
  79. #:key timeout)
  80. "Query the Jami D-Bus ConfigurationManager interface with METHOD applied to
  81. ARGUMENTS. TIMEOUT can optionally be provided as a value in seconds."
  82. (unless (%current-dbus-connection)
  83. (initialize-dbus-connection!))
  84. (call-dbus-method method
  85. #:path "/cx/ring/Ring/ConfigurationManager"
  86. #:destination "cx.ring.Ring"
  87. #:interface "cx.ring.Ring.ConfigurationManager"
  88. #:arguments arguments
  89. #:timeout timeout))
  90. ;;; The following methods are for internal use; they make use of the account
  91. ;;; ID, an implementation detail of Jami the user should not need to be
  92. ;;; concerned with.
  93. (define (get-account-ids)
  94. "Return the available Jami account identifiers (IDs). Account IDs are an
  95. implementation detail used to identify the accounts in Jami."
  96. (vector->list (call-configuration-manager-method "getAccountList")))
  97. (define (id->account-details id)
  98. "Retrieve the account data associated with the given account ID."
  99. (vector->list (call-configuration-manager-method "getAccountDetails"
  100. (list id))))
  101. (define (id->volatile-account-details id)
  102. "Retrieve the account data associated with the given account ID."
  103. (vector->list (call-configuration-manager-method "getVolatileAccountDetails"
  104. (list id))))
  105. (define (id->account id)
  106. "Retrieve the complete account data associated with the given account ID."
  107. (append (id->volatile-account-details id)
  108. (id->account-details id)))
  109. (define %username-to-id-cache #f)
  110. (define (invalidate-username-to-id-cache!)
  111. (set! %username-to-id-cache #f))
  112. (define (username->id username)
  113. "Return the first account ID corresponding to USERNAME."
  114. (unless (assoc-ref %username-to-id-cache username)
  115. (set! %username-to-id-cache
  116. (append-map
  117. (lambda (id)
  118. (let* ((account (id->account id))
  119. (username (assoc-ref account "Account.username"))
  120. (registered-name (assoc-ref account
  121. "Account.registeredName")))
  122. `(,@(if username
  123. (list (cons username id))
  124. '())
  125. ,@(if registered-name
  126. (list (cons registered-name id))
  127. '()))))
  128. (get-account-ids))))
  129. (or (assoc-ref %username-to-id-cache username)
  130. (let ((message (format #f "no account ID for ~:[username~;fingerprint~]"
  131. (account-fingerprint? username))))
  132. (error message username))))
  133. (define (account->username account)
  134. "Return the public key fingerprint of ACCOUNT."
  135. (assoc-ref account "Account.username"))
  136. (define (id->username id)
  137. "Return the public key fingerprint corresponding to account with ID, else #f."
  138. (account->username (id->account id)))
  139. (define (get-accounts)
  140. "Return the list of all accounts, as a list of alists."
  141. (map id->account (get-account-ids)))
  142. (define (get-usernames)
  143. "Return the list of the usernames associated with the present accounts."
  144. (map account->username (get-accounts)))
  145. (define (username->account username)
  146. "Return the first account associated with USERNAME, else #f.
  147. USERNAME can be either the account 40 characters public key fingerprint or a
  148. registered username."
  149. (find (lambda (account)
  150. (member username
  151. (list (assoc-ref account "Account.username")
  152. (assoc-ref account "Account.registeredName"))))
  153. (get-accounts)))
  154. (define (add-account archive)
  155. "Import the Jami account ARCHIVE and return its account ID. The archive
  156. should *not* be encrypted with a password. Return the username associated
  157. with the account."
  158. (invalidate-username-to-id-cache!)
  159. (let ((id (call-configuration-manager-method
  160. "addAccount" (list `#(("Account.archivePath" . ,archive)
  161. ("Account.type" . "RING"))))))
  162. ;; The account information takes some time to be populated.
  163. (with-retries 20 1
  164. (let ((username (id->username id)))
  165. (if (and=> username (negate string-null?))
  166. username
  167. #f)))))
  168. (define (remove-account username)
  169. "Delete the Jami account associated with USERNAME, the account 40 characters
  170. fingerprint or a registered username."
  171. (let ((id (username->id username)))
  172. (call-configuration-manager-method "removeAccount" (list id)))
  173. (invalidate-username-to-id-cache!))
  174. (define* (username->contacts username)
  175. "Return the contacts associated with the account of USERNAME as two values;
  176. the first one being the regular contacts and the second one the banned
  177. contacts. USERNAME can be either the account 40 characters public key
  178. fingerprint or a registered username. The contacts returned are represented
  179. using their 40 characters fingerprint."
  180. (let* ((id (username->id username))
  181. ;; The contacts are returned as "aa{ss}", that is, an array of arrays
  182. ;; containing (string . string) pairs.
  183. (contacts (map vector->list
  184. (vector->list (call-configuration-manager-method
  185. "getContacts" (list id)))))
  186. (banned? (lambda (contact)
  187. (and=> (assoc-ref contact "banned")
  188. (cut string=? "true" <>))))
  189. (banned (filter banned? contacts))
  190. (not-banned (filter (negate banned?) contacts))
  191. (fingerprint (cut assoc-ref <> "id")))
  192. (values (map fingerprint not-banned)
  193. (map fingerprint banned))))
  194. (define* (remove-contact contact username #:key ban?)
  195. "Remove CONTACT, the 40 characters public key fingerprint of a contact, from
  196. the account associated with USERNAME (either a fingerprint or a registered
  197. username). When BAN? is true, also mark the contact as banned."
  198. (validate-fingerprint contact)
  199. (let ((id (username->id username)))
  200. (call-configuration-manager-method "removeContact" (list id contact ban?))))
  201. (define (add-contact contact username)
  202. "Add CONTACT, the 40 characters public key fingerprint of a contact, to the
  203. account of USERNAME (either a fingerprint or a registered username)."
  204. (validate-fingerprint contact)
  205. (let ((id (username->id username)))
  206. (call-configuration-manager-method "addContact" (list id contact))))
  207. (define* (set-account-details details username #:key timeout)
  208. "Set DETAILS, an alist containing the key value pairs to set for the account
  209. of USERNAME, a registered username or account fingerprint. The value of the
  210. parameters not provided are unchanged. TIMEOUT is a value in milliseconds to
  211. pass to the `call-configuration-manager-method' procedure."
  212. (let* ((id (username->id username))
  213. (current-details (id->account-details id))
  214. (updated-details (map (match-lambda
  215. ((key . value)
  216. (or (and=> (assoc-ref details key)
  217. (cut cons key <>))
  218. (cons key value))))
  219. current-details)))
  220. (call-configuration-manager-method
  221. "setAccountDetails" (list id (list->vector updated-details))
  222. #:timeout timeout)))
  223. (define (set-all-moderators enabled? username)
  224. "Set the 'AllModerators' property to enabled? for the account of USERNAME, a
  225. registered username or account fingerprint."
  226. (let ((id (username->id username)))
  227. (call-configuration-manager-method "setAllModerators" (list id enabled?))))
  228. (define (username->all-moderators? username)
  229. "Return the 'AllModerators' property for the account of USERNAME, a
  230. registered username or account fingerprint."
  231. (let ((id (username->id username)))
  232. (call-configuration-manager-method "isAllModerators" (list id))))
  233. (define (username->moderators username)
  234. "Return the moderators for the account of USERNAME, a registered username or
  235. account fingerprint."
  236. (let* ((id (username->id username)))
  237. (vector->list (call-configuration-manager-method "getDefaultModerators"
  238. (list id)))))
  239. (define (set-moderator contact enabled? username)
  240. "Set the moderator flag to ENABLED? for CONTACT, the 40 characters public
  241. key fingerprint of a contact for the account of USERNAME, a registered
  242. username or account fingerprint."
  243. (validate-fingerprint contact)
  244. (let* ((id (username->id username)))
  245. (call-configuration-manager-method "setDefaultModerator"
  246. (list id contact enabled?))))
  247. (define (disable-account username)
  248. "Disable the account known by USERNAME, a registered username or account
  249. fingerprint."
  250. (set-account-details '(("Account.enable" . "false")) username
  251. ;; Waiting for the reply on this command takes a very
  252. ;; long time that trips the default D-Bus timeout value
  253. ;; (25 s), for some reason.
  254. #:timeout 60))
  255. (define (enable-account username)
  256. "Enable the account known by USERNAME, a registered username or account
  257. fingerprint."
  258. (set-account-details '(("Account.enable" . "true")) username))
  259. ;;;
  260. ;;; Presentation procedures.
  261. ;;;
  262. (define (.->_ text)
  263. "Map each period character to underscore characters."
  264. (string-map (match-lambda
  265. (#\. #\_)
  266. (c c))
  267. text))
  268. (define (account-details->recutil account-details)
  269. "Serialize the account-details alist into a recutil string. Period
  270. characters in the keys are normalized to underscore to meet Recutils' format
  271. requirements."
  272. (define (pair->recutil-property pair)
  273. (match pair
  274. ((key . value)
  275. (string-append (.->_ key) ": " value))))
  276. (define sorted-account-details
  277. ;; Have the account username, display name and alias appear first, for
  278. ;; convenience.
  279. (let ((first-items '("Account.username"
  280. "Account.displayName"
  281. "Account.alias")))
  282. (append (map (cut assoc <> account-details) first-items)
  283. (fold alist-delete account-details first-items))))
  284. (string-join (map pair->recutil-property sorted-account-details) "\n"))