branch: elpa/mastodon commit 9a2bee2424c043fe688a17372b94bfe14816b5f6 Merge: 5308821c78 a1e525dacc Author: marty hiatt <martianhia...@disroot.org> Commit: marty hiatt <martianhia...@disroot.org>
Merge branch 'token-encrypt' into develop --- README.org | 24 +++++- lisp/mastodon-auth.el | 158 +++++++++++++++++++++++++++++-------- lisp/mastodon-client.el | 176 +++++++++++++++++++++++++++--------------- lisp/mastodon.el | 15 ++++ mastodon-index.org | 102 ++++++++++++------------ test/mastodon-auth-tests.el | 36 +++++++++ test/mastodon-client-tests.el | 147 +++++++++++++++++++---------------- 7 files changed, 449 insertions(+), 209 deletions(-) diff --git a/README.org b/README.org index fe1759bfa9..6c54bacfaf 100644 --- a/README.org +++ b/README.org @@ -121,8 +121,28 @@ can activate one at a time by changing those two variables and restarting Emacs. If you were using mastodon.el before 2FA was implemented and the above -steps do not work, delete the old file specified by -=mastodon-client--token-file= and restart Emacs and follow the steps again. +steps do not work, call =(mastodon-forget-all-logins)=, restart Emacs and +follow the steps again. + +**** encrypted access tokens (from 1.2.0) + +By default, user access tokens are now stored in the user's auth source +file (typically =~/.authinfo.gpg=, check the value of =auth-sources=). When +you first update to 2.0.0, or if you encounter issues due to old +credentials, call =(mastodon-forget-all-logins)= and then authenticate +again. If you don't want this behaviour, set =mastodon-auth-use-auth-source= +to nil. Entries will be stored encrypted in =mastodon-client--token-file= instead. + +And if for some reason you reauthenticate, you'll need to either remove the +entry in your auth sources file, or manually update the token in it after +doing so, as mastodon.el is unable to reliably update (or even remove) +entires. + +Finally, if you find you're asked for your key passphrase too often while +authenticating, consider setting =epa-file-encrypt-to= (for auth-source +encryption) and =plstore-encrypt-to= (for plstore encryption) to your +preferred key ID. + *** Timelines diff --git a/lisp/mastodon-auth.el b/lisp/mastodon-auth.el index 6e90b5348f..d79eb60e6a 100644 --- a/lisp/mastodon-auth.el +++ b/lisp/mastodon-auth.el @@ -2,6 +2,7 @@ ;; Copyright (C) 2017-2019 Johnson Denen ;; Copyright (C) 2021 Abhiseck Paira <abhiseckpa...@disroot.org> +;; Copyright (C) 2025 Marty Hiatt <mouse...@disroot.org> ;; Author: Johnson Denen <johnson.de...@gmail.com> ;; Maintainer: Marty Hiatt <mouse...@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el @@ -32,6 +33,8 @@ (require 'plstore) (require 'auth-source) (require 'json) +(require 'url) + (eval-when-compile (require 'subr-x)) ; for if-let* (autoload 'mastodon-client "mastodon-client") @@ -44,17 +47,32 @@ (autoload 'mastodon-http--get-json "mastodon-http") (autoload 'mastodon-http--post "mastodon-http") (autoload 'mastodon-return-credential-account "mastodon") +(autoload 'mastodon-client--general-read "mastodon-client") (defvar mastodon-instance-url) (defvar mastodon-client-scopes) (defvar mastodon-client-redirect-uri) (defvar mastodon-active-user) +(defvar mastodon-client--token-file) (defgroup mastodon-auth nil "Authenticate with Mastodon." :prefix "mastodon-auth-" :group 'mastodon) +(defcustom mastodon-auth-use-auth-source t + "Whether to use auth sources for user credentials. +If t, save and read user access token in the user's auth source +file (see `auth-sources'). If nil, use `mastodon-client--token-file' +instead. +If you change the value of this variable, call +`mastodon-forget-all-logins' and log in again. +If for some reason you generate a new token, you'll have to update your +auth souce file manually, or at least remove the entry and authenticate +again, as auth-source.el only provides unreliable tools for updating +entries." + :type 'boolean) + (defvar mastodon-auth-source-file nil "This variable is obsolete. This variable currently serves no purpose and will be removed in @@ -69,26 +87,25 @@ the future.") (defvar mastodon-auth--user-unaware " ** MASTODON.EL - NOTICE ** -It appears that you are not aware of the recent developments in -mastodon.el. In short we now require that you also set the -variable `mastodon-active-user' in your init file in addition to -`mastodon-instance-url'. +User variables not set: mastodon.el requires that you set both +`mastodon-active-user' and `mastodon-instance-url' in your init file. Please see its documentation to understand what value it accepts by running M-x describe-variable on it or visiting our web page: -https://codeberg.org/martianh/mastodon.el - -We apologize for the inconvenience. +https://codeberg.org/martianh/mastodon.el. ") (defun mastodon-auth--get-browser-login-url () "Return properly formed browser login url." - (mastodon-http--concat-params-to-url - (concat mastodon-instance-url "/oauth/authorize/") - `(("response_type" . "code") - ("redirect_uri" . ,mastodon-client-redirect-uri) - ("scope" . ,mastodon-client-scopes) - ("client_id" . ,(plist-get (mastodon-client) :client_id))))) + (let ((client-id (plist-get (mastodon-client) :client_id))) + (if (not client-id) + (error "Failed to set up client id") + (mastodon-http--concat-params-to-url + (concat mastodon-instance-url "/oauth/authorize/") + `(("response_type" . "code") + ("redirect_uri" . ,mastodon-client-redirect-uri) + ("scope" . ,mastodon-client-scopes) + ("client_id" . ,client-id)))))) (defvar mastodon-auth--explanation (format @@ -168,28 +185,72 @@ When ASK is absent return nil." (json-string (buffer-substring-no-properties (point) (point-max)))) (json-read-from-string json-string)))) +(defun mastodon-auth--plstore-token-check (&optional auth-source) + "Signal an error if plstore contains unencrypted access-token. +If AUTH-SOURCE, and if `mastodon-auth-use-auth-source' is non-nil, +return non-nil if it contains any access token. +Used to help users switch to the new encrypted auth token flow." + ;; FIXME: is it poss to move this plstore read to have one less read? + ;; e.g. inside of `mastodon-client--active-user'? the issue is that + ;; ideally we want to test "user-" entry, even if fetching "active-user" + ;; entry, so we would have to re-do the plstore read functions. + (when + (mastodon-auth--plstore-access-token-member auth-source) + (if auth-source + (user-error "Auth source storage of tokens is enabled,\ + but there is also an access token in your plstore.\ + If you're seeing this message after updating,\ + call `mastodon-forget-all-logins', and try again. + If you don't want to use auth sources,\ + also set `mastodon-auth-use-auth-source' to nil.\ + If this message is in error, contact us on the mastodon.el repo") + (user-error "Unencrypted access token in your plstore.\ + If you're seeing this message after updating,\ + call `mastodon-forget-all-logins', and log in again. + If this message is in error, contact us on the mastodon.el repo")))) + +(defun mastodon-auth--plstore-access-token-member (&optional auth-source) + "Return non-nil if the user entry of the plstore contains :access_token. +If AUTH-SOURCE, also check if it contains :secret-access_token." + (let* ((plstore (plstore-open (mastodon-client--token-file))) + (name (concat "user-" (mastodon-client--form-user-from-vars))) + ;; get alist like plstore.el does, so that keys will display with + ;; ":secret-" prefix if encrypted: + (alist (assoc name (plstore--get-merged-alist plstore)))) + (if (and auth-source mastodon-auth-use-auth-source) + (or (member :access_token alist) + (member :secret-access_token alist)) + (member :access_token alist)))) + (defun mastodon-auth--access-token () "Return the access token to use with `mastodon-instance-url'. Generate/save token if none known yet." - (cond (mastodon-auth--token-alist - ;; user variables are known and initialised. - (alist-get mastodon-instance-url mastodon-auth--token-alist nil nil #'string=)) - ((plist-get (mastodon-client--active-user) :access_token) - ;; user variables need to be read from plstore. - (push (cons mastodon-instance-url - (plist-get (mastodon-client--active-user) :access_token)) - mastodon-auth--token-alist) - (alist-get mastodon-instance-url mastodon-auth--token-alist nil nil #'string=)) - ((null mastodon-active-user) - ;; user not aware of 2FA-related changes and has not set - ;; `mastodon-active-user'. Make user aware and error out. - (mastodon-auth--show-notice mastodon-auth--user-unaware - "*mastodon-notice*") - (error "Variables not set properly")) - (t - ;; user access-token needs to fetched from the server and - ;; stored and variables initialised. - (mastodon-auth--handle-token-response (mastodon-auth--get-token))))) + (cond + (mastodon-auth--token-alist + ;; user variables are known and initialised. + (alist-get mastodon-instance-url + mastodon-auth--token-alist nil nil #'string=)) + ;; if auth source enabled, but we have an access token in plstore, + ;; error out and tell user to remove plstore and start over or disable + ;; auth source: + ((mastodon-auth--plstore-token-check)) + ((plist-get (mastodon-client--active-user) :access_token) + ;; user variables need to be read from plstore active-user entry. + (push (cons mastodon-instance-url + (plist-get (mastodon-client--active-user) :access_token)) + mastodon-auth--token-alist) + (alist-get mastodon-instance-url + mastodon-auth--token-alist nil nil #'string=)) + ((null mastodon-active-user) + ;; user not aware of 2FA-related changes and has not set + ;; `mastodon-active-user'. Make user aware and error out. + (mastodon-auth--show-notice mastodon-auth--user-unaware + "*mastodon-notice*") + (user-error "Variables not set properly")) + (t + ;; user access-token needs to fetched from the server and + ;; stored and variables initialised. + (mastodon-auth--handle-token-response (mastodon-auth--get-token))))) (defun mastodon-auth--handle-token-response (response) "Add token RESPONSE to `mastodon-auth--token-alist'. @@ -206,6 +267,39 @@ Handle any errors from the server." (error "Mastodon-auth--access-token: %s: %s" class error)) (_ (error "Unknown response from mastodon-auth--get-token!")))) +(defun mastodon-auth-source-get (user host &optional token create) + "Fetch an auth source token, searching by USER and HOST. +If CREATE, use TOKEN or prompt for it, and save it if there is no such entry. +Return a list of user, password/secret, and the item's save-function." + (let* ((auth-source-creation-prompts + '((secret . "%u access token: "))) + (source + (car + (auth-source-search :host host :user user + :require '(:user :secret) + :secret (if token token nil) + ;; "create" alone doesn't work here!: + :create (if create t nil))))) + (when source + (let ((creds + `(,(plist-get source :user) + ,(auth-info-password source) + ,(plist-get source :save-function)))) + (when create ;; call save function: + (when (functionp (nth 2 creds)) + (funcall (nth 2 creds)))) + creds)))) + +(defun mastodon-auth-source-token (url handle &optional token create) + "Parse URL, search auth sources with it, user HANDLE and TOKEN. +Calls `mastodon-auth-source-get', returns only the token. +If CREATE, create an entry is none is found." + (let ((host (url-host + (url-generic-parse-url url))) + (username (car (split-string handle "@")))) + (nth 1 + (mastodon-auth-source-get username host token create)))) + (defun mastodon-auth--get-account-name () "Request user credentials and return an account name." (alist-get 'acct diff --git a/lisp/mastodon-client.el b/lisp/mastodon-client.el index c0db3d6172..1ad6d2bed7 100644 --- a/lisp/mastodon-client.el +++ b/lisp/mastodon-client.el @@ -2,6 +2,7 @@ ;; Copyright (C) 2017-2019 Johnson Denen ;; Copyright (C) 2021 Abhiseck Paira <abhiseckpa...@disroot.org> +;; Copyright (C) 2025 Marty Hiatt <mouse...@disroot.org> ;; Author: Johnson Denen <johnson.de...@gmail.com> ;; Maintainer: Marty Hiatt <mouse...@disroot.org> ;; Homepage: https://codeberg.org/martianh/mastodon.el @@ -35,11 +36,14 @@ (defvar mastodon-instance-url) (defvar mastodon-active-user) +(defvar mastodon-auth-use-auth-source) (autoload 'mastodon-http--api "mastodon-http") (autoload 'mastodon-http--post "mastodon-http") +(autoload 'mastodon-auth-source-token "mastodon-auth") -(defcustom mastodon-client--token-file (concat user-emacs-directory "mastodon.plstore") +(defcustom mastodon-client--token-file + (concat user-emacs-directory "mastodon.plstore") "File path where Mastodon access tokens are stored." :group 'mastodon :type 'file) @@ -71,14 +75,23 @@ (defun mastodon-client--fetch () "Return JSON from `mastodon-client--register' call." - (with-current-buffer (mastodon-client--register) - (goto-char (point-min)) - (re-search-forward "^$" nil 'move) - (let ((json-object-type 'plist) - (json-key-type 'keyword) - (json-array-type 'vector) - (json-string (buffer-substring-no-properties (point) (point-max)))) - (json-read-from-string json-string)))) + (let ((buf (mastodon-client--register))) + (if (not buf) + (user-error "Client registration failed.\ + Is `mastodon-instance-url' correct?") + (with-current-buffer buf + (goto-char (point-min)) + (re-search-forward "^$" nil 'move) + (let* ((json-object-type 'plist) + (json-key-type 'keyword) + (json-array-type 'vector) + (json-string + (buffer-substring-no-properties (point) (point-max))) + (parsed + (json-read-from-string json-string))) + (if (eq :error (car parsed)) + (error "Error: %s" (cadr parsed)) + parsed)))))) (defun mastodon-client--token-file () "Return `mastodon-client--token-file'." @@ -86,19 +99,28 @@ (defun mastodon-client--store () "Store client_id and client_secret in `mastodon-client--token-file'. - -Make `mastodon-client--fetch' call to determine client values." - (let ((plstore (plstore-open (mastodon-client--token-file))) - (client (mastodon-client--fetch)) - ;; alexgriffith reported seeing ellipses in the saved output - ;; which indicate some output truncating. Nothing in `plstore-save' - ;; seems to ensure this cannot happen so let's do that ourselves: - (print-length nil) - (print-level nil)) - (plstore-put plstore (concat "mastodon-" mastodon-instance-url) client nil) +Make `mastodon-client--fetch' call to determine client values. +Return a plist of secret and non-secret key/val pairs." + (let* ((plstore (plstore-open (mastodon-client--token-file))) + (client (mastodon-client--fetch)) + (secrets `( :client_id ,(plist-get client :client_id) + :client_secret ,(plist-get client :client_secret))) + (sans-secrets + (dolist (x '(:client_id :client_secret) client) + (cl-remf client x))) + ;; alexgriffith reported seeing ellipses in the saved output + ;; which indicate some output truncating. Nothing in + ;; `plstore-save' seems to ensure this cannot happen so let's do + ;; that ourselves: + (print-length nil) + (print-level nil)) + (plstore-put plstore + (concat "mastodon-" mastodon-instance-url) + sans-secrets secrets) + ;; FIXME: breaks tests: prompts for gpg passphrase (plstore-save plstore) (plstore-close plstore) - client)) + (append secrets sans-secrets))) (defun mastodon-client--remove-key-from-plstore (plstore) "Remove KEY from PLSTORE." @@ -109,7 +131,10 @@ Make `mastodon-client--fetch' call to determine client values." (defun mastodon-client--read () "Retrieve client_id and client_secret from `mastodon-client--token-file'." (let* ((plstore (plstore-open (mastodon-client--token-file))) - (mastodon (plstore-get plstore (concat "mastodon-" mastodon-instance-url)))) + (mastodon + (plstore-get plstore + (concat "mastodon-" mastodon-instance-url)))) + (plstore-close plstore) (mastodon-client--remove-key-from-plstore mastodon))) (defun mastodon-client--general-read (key) @@ -117,38 +142,69 @@ Make `mastodon-client--fetch' call to determine client values." Return plist without the KEY." (let* ((plstore (plstore-open (mastodon-client--token-file))) (plstore-item (plstore-get plstore key))) + (plstore-close plstore) (mastodon-client--remove-key-from-plstore plstore-item))) (defun mastodon-client--make-user-details-plist () "Make a plist with current user details. Return it." - `(:username ,(mastodon-client--form-user-from-vars) - :instance ,mastodon-instance-url - :client_id ,(plist-get (mastodon-client) :client_id) - :client_secret ,(plist-get (mastodon-client) :client_secret))) + `( :username ,(mastodon-client--form-user-from-vars) + :instance ,mastodon-instance-url + :client_id ,(plist-get (mastodon-client) :client_id) + :client_secret ,(plist-get (mastodon-client) :client_secret))) (defun mastodon-client--store-access-token (token) - "Save TOKEN as :access_token in plstore of the current user. -Return the plist after the operation." + "Save TOKEN as :access_token encrypted in the plstore. +Return the plist after the operation. +If `mastodon-auth-use-auth-source', encrypt it in auth source file." (let* ((user-details (mastodon-client--make-user-details-plist)) (plstore (plstore-open (mastodon-client--token-file))) - (username (plist-get user-details :username)) - (plstore-value (setq user-details - (plist-put user-details :access_token token))) + (username (mastodon-client--form-user-from-vars)) + (key (concat "user-" username)) + (secrets `( :client_id ,(plist-get user-details :client_id) + :client_secret ,(plist-get user-details :client_secret))) + (sans-secrets + (dolist (x '(:client_id :client_secret) user-details) + (cl-remf user-details x))) (print-length nil) (print-level nil)) - (plstore-put plstore (concat "user-" username) plstore-value nil) + (if mastodon-auth-use-auth-source + ;; auth-source: + (progn + (mastodon-auth-source-token + mastodon-instance-url username token :create) + (plstore-put plstore key sans-secrets secrets)) + ;; plstore encrypted: + (plstore-put plstore key sans-secrets + (append secrets `(:access_token ,token)))) (plstore-save plstore) (plstore-close plstore) - plstore-value)) + (cdr (plstore-get plstore key)))) (defun mastodon-client--make-user-active (user-details) - "USER-DETAILS is a plist consisting of user details." - (let ((plstore (plstore-open (mastodon-client--token-file))) - (print-length nil) - (print-level nil)) - (plstore-put plstore "active-user" user-details nil) + "USER-DETAILS is a plist consisting of user details. +Save it to plstore under key \"active-user\". +If `mastodon-auth-use-auth-source' is non-nil, fetch the access token +from the user's auth source file and add it to the active user entry. +Return a plist of secret and non-secret key/val pairs." + (let* ((plstore (plstore-open (mastodon-client--token-file))) + (handle (plist-get user-details :username)) + (token + (if mastodon-auth-use-auth-source + (mastodon-auth-source-token mastodon-instance-url handle) + (plist-get user-details :access_token))) + (secrets `( :access_token ,token + :client_id ,(plist-get user-details :client_id) + :client_secret ,(plist-get user-details :client_secret))) + (deets (copy-sequence user-details)) + (sans-secrets + (dolist (x '(:client_id :client_secret :access_token) deets) + (cl-remf deets x))) + (print-length nil) + (print-level nil)) + (plstore-put plstore "active-user" sans-secrets secrets) (plstore-save plstore) - (plstore-close plstore))) + (plstore-close plstore) + (append secrets sans-secrets))) (defun mastodon-client--form-user-from-vars () "Create a username from user variable. Return that username. @@ -161,12 +217,12 @@ variables `mastodon-instance-url' and `mastodon-active-user'." (defun mastodon-client--make-current-user-active () "Make the user specified by user variables active user. Return the details (plist)." - (let ((username (mastodon-client--form-user-from-vars)) - user-plist) - (when (setq user-plist - (mastodon-client--general-read (concat "user-" username))) - (mastodon-client--make-user-active user-plist)) - user-plist)) + (let* ((username (mastodon-client--form-user-from-vars)) + (user-plist (mastodon-client--general-read + (concat "user-" username)))) + (when user-plist + (mastodon-client--make-user-active user-plist) + user-plist))) (defun mastodon-client--current-user-active-p () "Return user-details if the current user is active. @@ -180,28 +236,26 @@ Otherwise return nil." (defun mastodon-client--active-user () "Return the details of the currently active user. Details is a plist." - (let ((active-user-details mastodon-client--active-user-details-plist)) - (unless active-user-details - (setq active-user-details - (or (mastodon-client--current-user-active-p) - (mastodon-client--make-current-user-active))) + (or mastodon-client--active-user-details-plist (setq mastodon-client--active-user-details-plist - active-user-details)) - active-user-details)) + (or (mastodon-client--current-user-active-p) + (mastodon-client--make-current-user-active))))) (defun mastodon-client () "Return variable client secrets to use for `mastodon-instance-url'. -Read plist from `mastodon-client--token-file' if variable is nil. -Fetch and store plist if `mastodon-client--read' returns nil." +If `mastodon-client--client-details-alist' is nil, read plist from +`mastodon-client--token-file'. +Fetch and store plist if `mastodon-client--read' returns nil. +Return a plist." (let ((client-details - (cdr (assoc mastodon-instance-url mastodon-client--client-details-alist)))) - (unless client-details - (setq client-details - (or (mastodon-client--read) - (mastodon-client--store))) - (push (cons mastodon-instance-url client-details) - mastodon-client--client-details-alist)) - client-details)) + (cdr (assoc mastodon-instance-url + mastodon-client--client-details-alist)))) + (or client-details + (let ((client-details (or (mastodon-client--read) + (mastodon-client--store)))) + (push (cons mastodon-instance-url client-details) + mastodon-client--client-details-alist) + client-details)))) (provide 'mastodon-client) ;;; mastodon-client.el ends here diff --git a/lisp/mastodon.el b/lisp/mastodon.el index c092f43850..daf6f28dba 100644 --- a/lisp/mastodon.el +++ b/lisp/mastodon.el @@ -105,6 +105,7 @@ (defvar mastodon-tl--highlight-current-toot) (defvar mastodon-notifications--map) +(defvar mastodon-client--token-file) (defvar mastodon-notifications-grouped-types '("reblog" "favourite") ;; TODO: implement follow! @@ -195,6 +196,20 @@ and X others...\"." (interactive) (quit-window 'kill)) +;;;###autoload +(defun mastodon-forget-all-logins () + "Delete `mastodon-client--token-file'. +Also nil `mastodon-auth--token-alist'." + (interactive) + (when (y-or-n-p "Remove all saved login data?") + (if (not (file-exists-p mastodon-client--token-file)) + (message "No plstore file") + (delete-file mastodon-client--token-file) + (message "File %s deleted." mastodon-client--token-file)) + ;; nil some vars too: + (setq mastodon-client--active-user-details-plist nil) + (setq mastodon-auth--token-alist nil))) + (defvar mastodon-mode-map (let ((map (make-sparse-keymap))) ;; navigation inside a timeline diff --git a/mastodon-index.org b/mastodon-index.org index 7f0023ae5c..bcde1943f6 100644 --- a/mastodon-index.org +++ b/mastodon-index.org @@ -55,6 +55,7 @@ | C-M-q | mastodon-kill-all-buffers | Kill any and all open mastodon buffers, hopefully. | | Q | mastodon-kill-window | Quit window and delete helper. | | | mastodon-mode | Major mode for fediverse services using the Mastodon API. | +| | mastodon-forget-all-logins | Delete `mastodon-client--token-file'. | | | mastodon-notifications-clear-all | Clear all notifications. | | C-k | mastodon-notifications-clear-current | Dismiss the notification at point. | | | mastodon-notifications-cycle-type | Cycle the current notifications view. | @@ -135,6 +136,7 @@ | ! | mastodon-tl-fold-post-toggle | Toggle the folding status of the toot at point. | | | mastodon-tl-follow-tag | Prompt for a tag (from post at point) and follow it. | | W | mastodon-tl-follow-user | Query for USER-HANDLE from current status and follow that user. | +| | mastodon-tl-follow-user-by-handle | Prompt for a USER-HANDLE and follow that user. | | | mastodon-tl-follow-user-disable-boosts | Prompt for a USER-HANDLE, and disable display of boosts in home timeline. | | | mastodon-tl-follow-user-enable-boosts | Prompt for a USER-HANDLE, and enable display of boosts in home timeline. | | ' | mastodon-tl-followed-tags-timeline | Open a timeline of multiple tags. | @@ -274,53 +276,55 @@ #+end_src #+RESULTS: -| Custom variable | Description | -|----------------------------------------------------+-------------------------------------------------------------------------------| -| mastodon-active-user | Username of the active user. | -| mastodon-client--token-file | File path where Mastodon access tokens are stored. | -| mastodon-group-notifications | Whether to use grouped notifications. | -| mastodon-images-in-notifs | Whether to display attached images in notifications. | -| mastodon-instance-url | Base URL for the fediverse instance you want to be active. | -| mastodon-media--avatar-height | Height of the user avatar images (if shown). | -| mastodon-media--enable-image-caching | Whether images should be cached. | -| mastodon-media--hide-sensitive-media | Whether media marked as sensitive should be hidden. | -| mastodon-media--preview-max-height | Max height of any media attachment preview to be shown in timelines. | -| mastodon-mode-hook | Hook run when entering Mastodon mode. | -| mastodon-notifications-grouped-names-count | The number of notification authors to display. | -| mastodon-profile-mode-hook | Hook run after entering or leaving `mastodon-profile-mode'. | -| mastodon-profile-note-in-foll-reqs | If non-nil, show a user's profile note in follow request notifications. | -| mastodon-profile-note-in-foll-reqs-max-length | The max character length for user profile note in follow requests. | -| mastodon-profile-update-mode-hook | Hook run after entering or leaving `mastodon-profile-update-mode'. | -| mastodon-search-mode-hook | Hook run after entering or leaving `mastodon-search-mode'. | -| mastodon-tl--display-caption-not-url-when-no-media | Display an image's caption rather than URL. | -| mastodon-tl--display-media-p | A boolean value stating whether to show media in timelines. | -| mastodon-tl--enable-proportional-fonts | Nonnil to enable using proportional fonts when rendering HTML. | -| mastodon-tl--enable-relative-timestamps | Whether to show relative (to the current time) timestamps. | -| mastodon-tl--expand-content-warnings | Whether to expand content warnings by default. | -| mastodon-tl--fold-toots-at-length | Length, in characters, to fold a toot at. | -| mastodon-tl--hide-replies | Whether to hide replies from the timelines. | -| mastodon-tl--highlight-current-toot | Whether to highlight the toot at point. Uses `cursor-face' special property. | -| mastodon-tl--load-full-sized-images-in-emacs | Whether to load full-sized images inside Emacs. | -| mastodon-tl--no-fill-on-render | Non-nil to disable filling by shr.el while rendering toot body. | -| mastodon-tl--remote-local-domains | A list of domains to view the local timelines of. | -| mastodon-tl--show-avatars | Whether to enable display of user avatars in timelines. | -| mastodon-tl--show-stats | Whether to show toot stats (faves, boosts, replies counts). | -| mastodon-tl--symbols | A set of symbols (and fallback strings) to be used in timeline. | +| Custom variable | Description | +|----------------------------------------------------+------------------------------------------------------------------------------| +| mastodon-active-user | Username of the active user. | +| mastodon-auth-encrypt-access-token | Whether to encrypt the user's authentication token in the plstore. | +| mastodon-auth-use-auth-source | Whether to use auth sources for user credentials. | +| mastodon-client--token-file | File path where Mastodon access tokens are stored. | +| mastodon-group-notifications | Whether to use grouped notifications. | +| mastodon-images-in-notifs | Whether to display attached images in notifications. | +| mastodon-instance-url | Base URL for the fediverse instance you want to be active. | +| mastodon-media--avatar-height | Height of the user avatar images (if shown). | +| mastodon-media--enable-image-caching | Whether images should be cached. | +| mastodon-media--hide-sensitive-media | Whether media marked as sensitive should be hidden. | +| mastodon-media--preview-max-height | Max height of any media attachment preview to be shown in timelines. | +| mastodon-mode-hook | Hook run when entering Mastodon mode. | +| mastodon-notifications-grouped-names-count | The number of notification authors to display. | +| mastodon-profile-mode-hook | Hook run after entering or leaving `mastodon-profile-mode'. | +| mastodon-profile-note-in-foll-reqs | If non-nil, show a user's profile note in follow request notifications. | +| mastodon-profile-note-in-foll-reqs-max-length | The max character length for user profile note in follow requests. | +| mastodon-profile-update-mode-hook | Hook run after entering or leaving `mastodon-profile-update-mode'. | +| mastodon-search-mode-hook | Hook run after entering or leaving `mastodon-search-mode'. | +| mastodon-tl--display-caption-not-url-when-no-media | Display an image's caption rather than URL. | +| mastodon-tl--display-media-p | A boolean value stating whether to show media in timelines. | +| mastodon-tl--enable-proportional-fonts | Nonnil to enable using proportional fonts when rendering HTML. | +| mastodon-tl--enable-relative-timestamps | Whether to show relative (to the current time) timestamps. | +| mastodon-tl--expand-content-warnings | Whether to expand content warnings by default. | +| mastodon-tl--fold-toots-at-length | Length, in characters, to fold a toot at. | +| mastodon-tl--hide-replies | Whether to hide replies from the timelines. | +| mastodon-tl--highlight-current-toot | Whether to highlight the toot at point. Uses `cursor-face' special property. | +| mastodon-tl--load-full-sized-images-in-emacs | Whether to load full-sized images inside Emacs. | +| mastodon-tl--no-fill-on-render | Non-nil to disable filling by shr.el while rendering toot body. | +| mastodon-tl--remote-local-domains | A list of domains to view the local timelines of. | +| mastodon-tl--show-avatars | Whether to enable display of user avatars in timelines. | +| mastodon-tl--show-stats | Whether to show toot stats (faves, boosts, replies counts). | +| mastodon-tl--symbols | A set of symbols (and fallback strings) to be used in timeline. | | mastodon-tl--tag-timeline-tags | A list of up to four tags for use with `mastodon-tl-followed-tags-timeline'. | -| mastodon-tl--tags-groups | A list containing lists of up to four tags each. | -| mastodon-tl--timeline-posts-count | Number of posts to display when loading a timeline. | -| mastodon-tl-position-after-update | Defines where `point' should be located after a timeline update. | -| mastodon-toot--attachment-height | Height of the attached images preview in the toot draft buffer. | -| mastodon-toot--completion-style-for-mentions | The company completion style to use for mentions. | -| mastodon-toot--default-media-directory | The default directory when prompting for a media file to upload. | -| mastodon-toot--default-reply-visibility | Default visibility settings when replying. | -| mastodon-toot--enable-completion | Whether to enable completion of mentions and hashtags. | -| mastodon-toot--enable-custom-instance-emoji | Whether to enable your instance's custom emoji by default. | -| mastodon-toot--proportional-fonts-compose | Nonnil to enable using proportional fonts in the compose buffer. | -| mastodon-toot--use-company-for-completion | Whether to enable company for completion. | -| mastodon-toot-display-orig-in-reply-buffer | Display a copy of the toot replied to in the compose buffer. | -| mastodon-toot-mode-hook | Hook run after entering or leaving `mastodon-toot-mode'. | -| mastodon-toot-orig-in-reply-length | Length to crop toot replied to in the compose buffer to. | -| mastodon-toot-poll-use-transient | Whether to use the transient menu to create a poll. | -| mastodon-toot-timestamp-format | Format to use for timestamps. | -| mastodon-use-emojify | Whether to use emojify.el to display emojis. | +| mastodon-tl--tags-groups | A list containing lists of up to four tags each. | +| mastodon-tl--timeline-posts-count | Number of posts to display when loading a timeline. | +| mastodon-tl-position-after-update | Defines where `point' should be located after a timeline update. | +| mastodon-toot--attachment-height | Height of the attached images preview in the toot draft buffer. | +| mastodon-toot--completion-style-for-mentions | The company completion style to use for mentions. | +| mastodon-toot--default-media-directory | The default directory when prompting for a media file to upload. | +| mastodon-toot--default-reply-visibility | Default visibility settings when replying. | +| mastodon-toot--enable-completion | Whether to enable completion of mentions and hashtags. | +| mastodon-toot--enable-custom-instance-emoji | Whether to enable your instance's custom emoji by default. | +| mastodon-toot--proportional-fonts-compose | Nonnil to enable using proportional fonts in the compose buffer. | +| mastodon-toot--use-company-for-completion | Whether to enable company for completion. | +| mastodon-toot-display-orig-in-reply-buffer | Display a copy of the toot replied to in the compose buffer. | +| mastodon-toot-mode-hook | Hook run after entering or leaving `mastodon-toot-mode'. | +| mastodon-toot-orig-in-reply-length | Length to crop toot replied to in the compose buffer to. | +| mastodon-toot-poll-use-transient | Whether to use the transient menu to create a poll. | +| mastodon-toot-timestamp-format | Format to use for timestamps. | +| mastodon-use-emojify | Whether to use emojify.el to display emojis. | diff --git a/test/mastodon-auth-tests.el b/test/mastodon-auth-tests.el index af410364cb..5ce9910534 100644 --- a/test/mastodon-auth-tests.el +++ b/test/mastodon-auth-tests.el @@ -75,3 +75,39 @@ (with-mock (mock (mastodon-client--active-user)) (should-error (mastodon-auth--access-token))))) + +(ert-deftest mastodon-auth-plstore-token-check () + (let ((mastodon-instance-url "https://mastodon.example") + (mastodon-active-user "test8000") + (user-details ;; order changed for new encrypted auth flow: + '( :client_id "id" :client_secret "secret" + :access_token "token" + :username "test8000@mastodon.example" + :instance "https://mastodon.example")) + ;; save token to plstore encrypted: + (mastodon-auth-use-auth-source nil)) ;; FIXME: test auth source + ;; setup plstore: store access token + (with-mock + (mock (mastodon-client) => '(:client_id "id" :client_secret "secret")) + (mock (mastodon-client--token-file) => "stubfile.plstore") + (should + (equal (mastodon-client--store-access-token "token") + user-details)) + ;; should non-nil if we check with auth-source: + ;; because we saved with non auth-source: + (should + (equal + (let ((mastodon-auth-use-auth-source t)) + (mastodon-auth--plstore-access-token-member :auth-source)) + '(:secret-access_token t :username "test8000@mastodon.example" + :instance "https://mastodon.example"))) + ;; should nil if we don't check with auth source: + (should + (equal + (mastodon-auth--plstore-access-token-member) + nil))) + ;; FIXME: ideally we would also mock up a non-encrypted plstore and + ;; test against it too, as that's the work we really want + ;; `mastodon-auth--plstore-access-token-member' to do + ;; but we don't currently have a way to mock one up. + (delete-file "stubfile.plstore"))) diff --git a/test/mastodon-client-tests.el b/test/mastodon-client-tests.el index b302ed6e5b..83dc106d48 100644 --- a/test/mastodon-client-tests.el +++ b/test/mastodon-client-tests.el @@ -1,6 +1,8 @@ ;;; mastodon-client-test.el --- Tests for mastodon-client.el -*- lexical-binding: nil -*- (require 'el-mock) +(require 'mastodon-client) +(require 'mastodon-http) (ert-deftest mastodon-client--register () "Should POST to /apps." @@ -19,19 +21,22 @@ "Should return client registration JSON." (with-temp-buffer (with-mock - (mock (mastodon-client--register) => (progn - (insert "\n\n{\"foo\":\"bar\"}") - (current-buffer))) - (should (equal (mastodon-client--fetch) '(:foo "bar")))))) - + (mock (mastodon-client--register) => (progn + (insert "\n\n{\"foo\":\"bar\"}") + (current-buffer))) + (should (equal (mastodon-client--fetch) '(:foo "bar")))))) + +;; FIXME: broken by new encrypted plstore flow +;; (asks for gpg passphrase) +;; otherwise test passes (ert-deftest mastodon-client--store () "Test the value `mastodon-client--store' returns/stores." (let ((mastodon-instance-url "http://mastodon.example") (plist '(:client_id "id" :client_secret "secret"))) (with-mock - (mock (mastodon-client--token-file) => "stubfile.plstore") - (mock (mastodon-client--fetch) => plist) - (should (equal (mastodon-client--store) plist))) + (mock (mastodon-client--token-file) => "stubfile.plstore") + (mock (mastodon-client--fetch) => plist) + (should (equal (mastodon-client--store) plist))) (let* ((plstore (plstore-open "stubfile.plstore")) (client (mastodon-client--remove-key-from-plstore (plstore-get plstore "mastodon-http://mastodon.example")))) @@ -40,48 +45,47 @@ ;; clean up - delete the stubfile (delete-file "stubfile.plstore")))) - (ert-deftest mastodon-client--read-finds-match () "Should return mastodon client from `mastodon-token-file' if it exists." (let ((mastodon-instance-url "http://mastodon.example")) (with-mock - (mock (mastodon-client--token-file) => "fixture/client.plstore") - (should (equal (mastodon-client--read) - '(:client_id "id2" :client_secret "secret2")))))) + (mock (mastodon-client--token-file) => "fixture/client.plstore") + (should (equal (mastodon-client--read) + '(:client_id "id2" :client_secret "secret2")))))) (ert-deftest mastodon-client--general-read-finds-match () (with-mock - (mock (mastodon-client--token-file) => "fixture/client.plstore") - (should (equal (mastodon-client--general-read "user-test8000@mastodon.example") - '(:username "test8000@mastodon.example" - :instance "http://mastodon.example" - :client_id "id2" :client_secret "secret2" - :access_token "token2"))))) + (mock (mastodon-client--token-file) => "fixture/client.plstore") + (should (equal (mastodon-client--general-read "user-test8000@mastodon.example") + '(:username "test8000@mastodon.example" + :instance "http://mastodon.example" + :client_id "id2" :client_secret "secret2" + :access_token "token2"))))) (ert-deftest mastodon-client--general-read-finds-no-match () (with-mock - (mock (mastodon-client--token-file) => "fixture/client.plstore") - (should (equal (mastodon-client--general-read "nonexistant-key") - nil)))) + (mock (mastodon-client--token-file) => "fixture/client.plstore") + (should (equal (mastodon-client--general-read "nonexistant-key") + nil)))) (ert-deftest mastodon-client--general-read-empty-store () (with-mock - (mock (mastodon-client--token-file) => "fixture/empty.plstore") - (should (equal (mastodon-client--general-read "something") - nil)))) + (mock (mastodon-client--token-file) => "fixture/empty.plstore") + (should (equal (mastodon-client--general-read "something") + nil)))) (ert-deftest mastodon-client--read-finds-no-match () "Should return mastodon client from `mastodon-token-file' if it exists." (let ((mastodon-instance-url "http://mastodon.social")) (with-mock - (mock (mastodon-client--token-file) => "fixture/client.plstore") - (should (equal (mastodon-client--read) nil))))) + (mock (mastodon-client--token-file) => "fixture/client.plstore") + (should (equal (mastodon-client--read) nil))))) (ert-deftest mastodon-client--read-empty-store () "Should return nil if mastodon client is not present in the plstore." (with-mock - (mock (mastodon-client--token-file) => "fixture/empty.plstore") - (should (equal (mastodon-client--read) nil)))) + (mock (mastodon-client--token-file) => "fixture/empty.plstore") + (should (equal (mastodon-client--read) nil)))) (ert-deftest mastodon-client--client-set-and-matching () "Should return `mastondon-client' if `mastodon-client--client-details-alist' is non-nil and instance url is included." @@ -95,32 +99,32 @@ (let ((mastodon-instance-url "http://mastodon.example") (mastodon-client--client-details-alist '(("http://other.example" :wrong)))) (with-mock - (mock (mastodon-client--read) => '(:client_id "foo" :client_secret "bar")) - (should (equal (mastodon-client) '(:client_id "foo" :client_secret "bar"))) - (should (equal mastodon-client--client-details-alist - '(("http://mastodon.example" :client_id "foo" :client_secret "bar") - ("http://other.example" :wrong))))))) + (mock (mastodon-client--read) => '(:client_id "foo" :client_secret "bar")) + (should (equal (mastodon-client) '(:client_id "foo" :client_secret "bar"))) + (should (equal mastodon-client--client-details-alist + '(("http://mastodon.example" :client_id "foo" :client_secret "bar") + ("http://other.example" :wrong))))))) (ert-deftest mastodon-client--client-unset () "Should read from `mastodon-token-file' if available." (let ((mastodon-instance-url "http://mastodon.example") (mastodon-client--client-details-alist nil)) (with-mock - (mock (mastodon-client--read) => '(:client_id "foo" :client_secret "bar")) - (should (equal (mastodon-client) '(:client_id "foo" :client_secret "bar"))) - (should (equal mastodon-client--client-details-alist - '(("http://mastodon.example" :client_id "foo" :client_secret "bar"))))))) + (mock (mastodon-client--read) => '(:client_id "foo" :client_secret "bar")) + (should (equal (mastodon-client) '(:client_id "foo" :client_secret "bar"))) + (should (equal mastodon-client--client-details-alist + '(("http://mastodon.example" :client_id "foo" :client_secret "bar"))))))) (ert-deftest mastodon-client--client-unset-and-not-in-storage () "Should store client data in plstore if it can't be read." (let ((mastodon-instance-url "http://mastodon.example") (mastodon-client--client-details-alist nil)) (with-mock - (mock (mastodon-client--read)) - (mock (mastodon-client--store) => '(:client_id "foo" :client_secret "baz")) - (should (equal (mastodon-client) '(:client_id "foo" :client_secret "baz"))) - (should (equal mastodon-client--client-details-alist - '(("http://mastodon.example" :client_id "foo" :client_secret "baz"))))))) + (mock (mastodon-client--read)) + (mock (mastodon-client--store) => '(:client_id "foo" :client_secret "baz")) + (should (equal (mastodon-client) '(:client_id "foo" :client_secret "baz"))) + (should (equal mastodon-client--client-details-alist + '(("http://mastodon.example" :client_id "foo" :client_secret "baz"))))))) (ert-deftest mastodon-client--form-user-from-vars () (let ((mastodon-active-user "test9000") @@ -134,41 +138,54 @@ (mastodon-instance-url "https://mastodon.example")) ;; when the current user /is/ the active user (with-mock - (mock (mastodon-client--general-read "active-user") => '(:username "test9000@mastodon.example" :client_id "id1")) - (should (equal (mastodon-client--current-user-active-p) - '(:username "test9000@mastodon.example" :client_id "id1")))) + (mock (mastodon-client--general-read "active-user") => '(:username "test9000@mastodon.example" :client_id "id1")) + (should (equal (mastodon-client--current-user-active-p) + '(:username "test9000@mastodon.example" :client_id "id1")))) ;; when the current user is /not/ the active user (with-mock - (mock (mastodon-client--general-read "active-user") => '(:username "user@other.example" :client_id "id1")) - (should (null (mastodon-client--current-user-active-p)))))) + (mock (mastodon-client--general-read "active-user") => '(:username "user@other.example" :client_id "id1")) + (should (null (mastodon-client--current-user-active-p)))))) +;; FIXME: broken by new encrypted plstore flow +;; (asks for gpg passphrase) +;; otherwise test passes (ert-deftest mastodon-client--store-access-token () (let ((mastodon-instance-url "https://mastodon.example") (mastodon-active-user "test8000") - (user-details - '(:username "test8000@mastodon.example" - :instance "https://mastodon.example" - :client_id "id" :client_secret "secret" - :access_token "token"))) + (user-details ;; order changed for new encrypted auth flow: + '( :client_id "id" :client_secret "secret" + :access_token "token" + :username "test8000@mastodon.example" + :instance "https://mastodon.example")) + (mastodon-auth-use-auth-source nil)) ;; FIXME: test auth source ;; test if mastodon-client--store-access-token /returns/ right ;; value (with-mock - (mock (mastodon-client) => '(:client_id "id" :client_secret "secret")) - (mock (mastodon-client--token-file) => "stubfile.plstore") - (should (equal (mastodon-client--store-access-token "token") - user-details))) + (mock (mastodon-client) => '(:client_id "id" :client_secret "secret")) + (mock (mastodon-client--token-file) => "stubfile.plstore") + (should (equal (mastodon-client--store-access-token "token") + user-details))) ;; test if mastodon-client--store-access-token /stores/ right value (with-mock - (mock (mastodon-client--token-file) => "stubfile.plstore") - (should (equal (mastodon-client--general-read - "user-test8000@mastodon.example") - user-details))) + (mock (mastodon-client--token-file) => "stubfile.plstore") + (should (equal (mastodon-client--general-read + "user-test8000@mastodon.example") + user-details))) (delete-file "stubfile.plstore"))) +;; FIXME: broken by new encrypted plstore flow +;; (asks for gpg passphrase) +;; otherwise test passes (ert-deftest mastodon-client--make-user-active () - (let ((user-details '(:username "test@mastodon.example"))) + ;; match new encrypted plstore return value: + (let ((user-details '( :access_token nil + :client_id nil + :client_secret nil + :username "test@mastodon.example")) + (mastodon-auth-use-auth-source nil)) ;; FIXME: test auth source (with-mock - (mock (mastodon-client--token-file) => "stubfile.plstore") - (mastodon-client--make-user-active user-details) - (should (equal (mastodon-client--general-read "active-user") - user-details))))) + (mock (mastodon-client--token-file) => "stubfile.plstore") + (mastodon-client--make-user-active user-details) + (should (equal (mastodon-client--general-read "active-user") + user-details))) + (delete-file "stubfile.plstore")))