branch: elpa/mastodon commit 310e401c8da8358e619c1ec59fd42fc059493309 Merge: fd59828c89 cc252f0c1c Author: marty hiatt <martianhia...@disroot.org> Commit: marty hiatt <martianhia...@disroot.org>
Merge branch 'develop' --- README.org | 37 +++++- lisp/.dir-locals.el | 3 +- lisp/mastodon-auth.el | 158 +++++++++++++++++++------ lisp/mastodon-client.el | 176 ++++++++++++++++++---------- lisp/mastodon-http.el | 156 ++++++++++++------------- lisp/mastodon-media.el | 30 +++-- lisp/mastodon-notifications.el | 34 +++--- lisp/mastodon-tl.el | 255 +++++++++++++++++++++++++++++++++-------- lisp/mastodon-toot.el | 13 ++- lisp/mastodon.el | 29 ++++- mastodon-index.org | 107 +++++++++-------- test/mastodon-auth-tests.el | 36 ++++++ test/mastodon-client-tests.el | 147 +++++++++++++----------- test/mastodon-http-tests.el | 36 +++--- test/mastodon-toot-tests.el | 1 + 15 files changed, 831 insertions(+), 387 deletions(-) diff --git a/README.org b/README.org index e2bbe7fdde..1b80e59c3d 100644 --- a/README.org +++ b/README.org @@ -121,8 +121,37 @@ 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 2.0.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)= to remove the old +mastodon.el plstore, and then authenticate again. If you don't want to use +the auth source file, set =mastodon-auth-use-auth-source= to nil. Entries +will instead be stored encrypted in =mastodon-client--token-file=, a plstore. + +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. + +The format for a mastodon.el auth source entry is as follows: + +=machine INSTANCE login USERNAME password AUTHTOKEN= + +with the token being what you copy from the browser when authenticating. +If you have =auth-source-save-behavior= set to nil, you'll also need to add +such an entry manually. + +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 @@ -432,6 +461,10 @@ available at https://holgerschurig.github.io/en/emacs-mastodon-hydra/. A simple code snippet to enable narrowing to current item in timelines: http://takeonrules.com/2024/10/31/hacking-on-mastodon-emacs-package-to-narrow-viewing/ +**** Sachac's config goodies + +The incomparable sachac has a bunch of =mastodon.el= extensions and goodies in their literate config, available here: https://sachachua.com/dotemacs/index.html#mastodon. + *** Live-updating timelines: =mastodon-async-mode= (code taken from [[https://github.com/alexjgriffith/mastodon-future.el][mastodon-future]].) diff --git a/lisp/.dir-locals.el b/lisp/.dir-locals.el index da012d6c7f..c1e0375993 100644 --- a/lisp/.dir-locals.el +++ b/lisp/.dir-locals.el @@ -2,5 +2,4 @@ ;;; For more information see (info "(emacs) Directory Variables") ((nil . ((indent-tabs-mode . nil))) - (emacs-lisp-mode . ((elisp-flymake-byte-compile-load-path . load-path) - (package-lint-main-file . "mastodon.el")))) + (emacs-lisp-mode . ((package-lint-main-file . "mastodon.el")))) diff --git a/lisp/mastodon-auth.el b/lisp/mastodon-auth.el index 6e90b5348f..2902c3195c 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,6 +47,8 @@ (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") +(autoload 'mastodon-client--token-file "mastodon-client") (defvar mastodon-instance-url) (defvar mastodon-client-scopes) @@ -55,6 +60,19 @@ :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-http.el b/lisp/mastodon-http.el index 98c2607950..a55097dff2 100644 --- a/lisp/mastodon-http.el +++ b/lisp/mastodon-http.el @@ -31,8 +31,8 @@ ;;; Code: (require 'json) -(require 'request) ; for attachments upload (require 'url) +(require 'url-http) (require 'shr) (defvar mastodon-instance-url) @@ -41,6 +41,7 @@ (autoload 'mastodon-auth--access-token "mastodon-auth") (autoload 'mastodon-toot--update-status-fields "mastodon-toot") +(autoload 'url-insert "url-handlers") (defvar mastodon-http--api-version "v1") @@ -57,23 +58,6 @@ Optionally specify VERSION in format vX." "Return Mastodon API v2 URL for ENDPOINT." (mastodon-http--api endpoint "v2")) -(defun mastodon-http--response () - "Capture response buffer content as string." - (with-current-buffer (current-buffer) - (buffer-substring-no-properties (point-min) (point-max)))) - -(defun mastodon-http--response-body (pattern) - "Return substring matching PATTERN from `mastodon-http--response'." - (let ((resp (mastodon-http--response))) - (string-match pattern resp) - (match-string 0 resp))) - -(defun mastodon-http--status () - "Return HTTP Response Status Code from `mastodon-http--response'." - (let* ((status-line (mastodon-http--response-body "^HTTP/1.*$"))) - (string-match "[0-9][0-9][0-9]" status-line) - (match-string 0 status-line))) - (defun mastodon-http--url-retrieve-synchronously (url &optional silent) "Retrieve URL asynchronously. This is a thin abstraction over the system @@ -86,13 +70,16 @@ SILENT means don't message." (defun mastodon-http--triage (response success) "Determine if RESPONSE was successful. -Call SUCCESS if successful. Message status and JSON error from -RESPONSE if unsuccessful." +Call SUCCESS on RESPONSE if successful. Message status and JSON error +from RESPONSE if unsuccessful." (let ((status (with-current-buffer response - (mastodon-http--status)))) - (if (string-prefix-p "2" status) + ;; FIXME: breaks tests, as url-http-end-of-headers not set + (url-http-parse-response)))) + (if (and (>= 200 status) + (<= status 299)) + ;; (string-prefix-p "2" (number-to-string status)) (funcall success response) - (if (string-prefix-p "404" status) + (if (= 404 status) (message "Error %s: page not found" status) (let ((json-response (with-current-buffer response (mastodon-http--process-json)))) @@ -360,73 +347,80 @@ PARAMS is an alist of any extra parameters to send with the request." Then run function CALLBACK with arguements CBARGS. Authorization header is included by default unless UNAUTHENTICED-P is non-nil." (mastodon-http--authorized-request "POST" - (let (;(request-timeout 5) ; this is from request.el no url.el! - (url-request-data (when params + (let ((url-request-data (when params (mastodon-http--build-params-string params)))) (with-temp-buffer (url-retrieve url callback cbargs))))) -;; TODO: test for curl first? +(defun mastodon-http--get-cb-data (status) + "Return data using `json-read' after a successful async request. +If STATUS includes an error, emit a message describing it and return nil." + (let* ((buf (current-buffer)) + (data (with-temp-buffer + (url-insert buf) + (goto-char (point-min)) + (json-read)))) + (if-let* ((error-thrown (plist-get status :error))) + ;; not necessarily a user error, but we want its functionality: + (user-error "%S %s" error-thrown (alist-get 'error data)) + data))) + +(defun mastodon-http--post-media-callback (status file caption buffer) + "Callback function called after posting FILE as an attachment with CAPTION. +The toot is being composed in BUFFER. See `url-retrieve' for STATUS." + (unwind-protect + (when-let* ((data (mastodon-http--get-cb-data status))) + (with-current-buffer buffer + (let ((id (alist-get 'id data))) + ;; update ids: + (if (not mastodon-toot--media-attachment-ids) + ;; add first id: + (push id mastodon-toot--media-attachment-ids) + ;; add new id to end of list to preserve order: + (push id (cdr + (last mastodon-toot--media-attachment-ids)))) + ;; pleroma, PUT the description: + ;; this is how the mangane akkoma web client does it + ;; and it seems easier than the other options! + (when (and caption + (not (string= caption (alist-get 'description data)))) + (let ((url (mastodon-http--api (format "media/%s" id)))) + ;; (message "PUTting image description") + (mastodon-http--put url `(("description" . ,caption))))) + (message "Uploading %s... (done)" file) + (mastodon-toot--update-status-fields)))) + (kill-buffer (current-buffer)))) + +(defun mastodon-http--post-media-prep-file (filename) + "Return the request data to upload FILENAME." + (with-temp-buffer + (set-buffer-multibyte nil) + (insert-file-contents-literally filename) + (let ((boundary (buffer-hash))) + (goto-char (point-min)) + (insert "--" boundary "\r\n" + (format "Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n\r\n" + (file-name-nondirectory filename))) + (goto-char (point-max)) + (insert "\r\n" "--" boundary "--" "\r\n") + `(,boundary . ,(buffer-substring-no-properties (point-min) (point-max)))))) + (defun mastodon-http--post-media-attachment (url filename caption) "Make POST request to upload FILENAME with CAPTION to the server's media URL. The upload is asynchronous. On succeeding, `mastodon-toot--media-attachment-ids' is set to the id(s) of the item uploaded, and `mastodon-toot--update-status-fields' is run." - (let* ((file (file-name-nondirectory filename)) - (request-backend 'curl) - (desc `(("description" . ,caption))) - (cb (cl-function - (lambda (&key data &allow-other-keys) - (when data - (let* ((id (alist-get 'id data))) - ;; update ids: - (if (not mastodon-toot--media-attachment-ids) - ;; add first id: - (push id mastodon-toot--media-attachment-ids) - ;; add new id to end of list to preserve order: - (push id (cdr - (last mastodon-toot--media-attachment-ids)))) - ;; pleroma, PUT the description: - ;; this is how the mangane akkoma web client does it - ;; and it seems easier than the other options! - (when (and caption - (not (string= caption (alist-get 'description data)))) - (let ((url (mastodon-http--api (format "media/%s" id)))) - ;; (message "PUTting image description") - (mastodon-http--put url desc))) - (message "Uploading %s... (done)" file) - (mastodon-toot--update-status-fields))))))) - (request - url - :type "POST" - :params desc - :files `(("file" . (,file :file ,filename - :mime-type "multipart/form-data"))) - :parser 'json-read - :headers `(("Authorization" . ,(concat "Bearer " - (mastodon-auth--access-token)))) - :sync nil - :success (apply-partially cb) - :error (cl-function - (lambda (&key error-thrown &allow-other-keys) - (cond - ;; handle curl errors first (eg 26, can't read file/path) - ;; because the '=' test below fails for them - ;; they have the form (error . error message 24) - ((not (proper-list-p error-thrown)) ; not dotted list - (message "Got error: %s. Shit went south." (cdr error-thrown))) - ;; handle mastodon api errors - ;; they have the form (error http 401) - ((= (car (last error-thrown)) 401) - (message "Got error: %s Unauthorized: The access token is invalid" - error-thrown)) - ((= (car (last error-thrown)) 422) - (message "Got error: %s Unprocessable entity: file or file\ - type is unsupported or invalid" - error-thrown)) - (t - (message "Got error: %s Shit went south" - error-thrown)))))))) + (mastodon-http--authorized-request "POST" + (let* ((data (mastodon-http--post-media-prep-file filename)) + (url-request-extra-headers + (append url-request-extra-headers ; auth set in macro + `(("Content-Type" . ,(format "multipart/form-data; boundary=%s" + (car data)))))) + (url-request-data (cdr data)) + (params `(("description" . ,caption))) + (url (mastodon-http--concat-params-to-url url params))) + (url-retrieve url #'mastodon-http--post-media-callback + `(,filename ,caption ,(current-buffer)))))) (provide 'mastodon-http) ;;; mastodon-http.el ends here diff --git a/lisp/mastodon-media.el b/lisp/mastodon-media.el index 09692c311b..87b58b8b38 100644 --- a/lisp/mastodon-media.el +++ b/lisp/mastodon-media.el @@ -332,7 +332,12 @@ image-data prop so it can be toggled." mastodon-media--sensitive-image-data nil t) sensitive-state hidden image-data ,image)))) -(defun mastodon-media--process-full-sized-image-response (status-plist url) +(defvar mastodon-media--attachments nil + "A list attachment details for full sized image view buffer. +The first element is the URL of the image displayed, followed by plists of details of all of a toot's attachments.") + +(defun mastodon-media--process-full-sized-image-response + (status-plist url attachments &optional prev-buf) ;; FIXME: refactor this with but not into ;; `mastodon-media--process-image-response'. "Callback function processing the `url-retrieve' response for URL. @@ -347,16 +352,25 @@ STATUS-PLIST is a plist of status events as per `url-retrieve'." ;; https://codeberg.org/martianh/mastodon.el/issues/540 (let* ((handle (mm-dissect-buffer t)) (image (mm-get-image handle)) - (str (image-property image :data))) - (with-current-buffer (get-buffer-create "*masto-image*") + (str (image-property image :data)) + (buf "*masto-image*")) + (with-current-buffer (get-buffer-create buf) (let ((inhibit-read-only t)) (erase-buffer) (insert-image image str) (special-mode) ; prevent image-mode loop bug - (image-mode) + (mastodon-image-mode) ;; for our keymap (goto-char (point-min)) - (switch-to-buffer-other-window (current-buffer)) - (image-transform-fit-both)))))) + (image-transform-fit-both) + ;; set image metadata for view cycling: + (setq-local mastodon-media--attachments (cons url attachments)))) + ;; switch to buf if not already viewing it: + (unless (equal buf prev-buf) + (switch-to-buffer-other-window buf)) + ;; display bindings if multiple images: + (when (< 1 (length (cdr mastodon-media--attachments))) + (message (substitute-command-keys + "\\`.'/\\`>'/\\`<right>' to cycle images")))))) (defun mastodon-media--image-or-cached (url process-fun args) "Fetch URL from cache or fro host. @@ -499,8 +513,8 @@ CAPTION is the image caption if provided. SENSITIVE is a flag from the item's JSON data." (let* ((help-echo-base (substitute-command-keys - (concat "\\`RET'/\\`i': load full image (prefix: copy URL), \\`+'/\\`-': zoom,\ - \\`r': rotate, \\`o': save preview" + (concat "\\`RET': load full image or play video,\ + \\`i' for image options" (when (not (eq sensitive :json-false)) ", \\`S': toggle sensitive media")))) (help-echo (if caption diff --git a/lisp/mastodon-notifications.el b/lisp/mastodon-notifications.el index b795db4041..e21aad3033 100644 --- a/lisp/mastodon-notifications.el +++ b/lisp/mastodon-notifications.el @@ -89,7 +89,7 @@ (defvar mastodon-group-notifications) (defvar mastodon-notifications-grouped-names-count) (defvar mastodon-tl--link-keymap) - +(defvar mastodon-tl--update-point) ;;; VARIABLES (defvar mastodon-notifications--map @@ -132,11 +132,6 @@ Notification types are named according to their name on the server.") ("Edited" . "their post")) "Alist of subjects for notification types.") -(defvar mastodon-notifications-grouped-types - '("reblog" "favourite") ;; TODO: implement follow! - "List of notification types for which grouping is implemented. -Used in `mastodon-notifications-get'") - (defvar mastodon-notifications--action-alist '((reblog . "Boosted") (favourite . "Favourited") @@ -587,23 +582,22 @@ When AVATAR, include the account's avatar image." "Display grouped notifications in JSON. NO-GROUP means don't render grouped notifications." ;; (setq masto-grouped-notifs json) - (if no-group - (cl-loop for x in json - do (mastodon-notifications--format-note x)) - (let ((groups (alist-get 'notification_groups json))) + (let ((start-pos (point))) + (if no-group + (cl-loop for x in json + do (mastodon-notifications--format-note x)) (cl-loop - for g in groups - for start-pos = (point) + for g in (alist-get 'notification_groups json) for accounts = (mastodon-notifications--group-accounts (alist-get 'sample_account_ids g) (alist-get 'accounts json)) for type = (alist-get 'type g) for status = (mastodon-notifications--status-or-event g type json) - do (mastodon-notifications--format-group-note g status accounts) - (when mastodon-tl--display-media-p - ;; images-in-notifs custom is handeld in - ;; `mastodon-tl--media-attachment', not here - (mastodon-media--inline-images start-pos (point))))))) + do (mastodon-notifications--format-group-note g status accounts))) + (when mastodon-tl--display-media-p + ;; images-in-notifs custom is handeld in + ;; `mastodon-tl--media-attachment', not here + (mastodon-media--inline-images start-pos (point))))) (defun mastodon-notifications--status-or-event (group type json) "Return a notification's status or event data. @@ -618,6 +612,10 @@ Using GROUP data, notification TYPE, and overall notifs JSON." 'id (alist-get 'statuses json)))) +(defun mastodon-notifications--empty-group-json-p (json) + "Non-nil if JSON is empty grouped notifs data." + (equal json '((accounts) (statuses) (notification_groups)))) + (defun mastodon-notifications--timeline (json &optional type update) "Format JSON in Emacs buffer. Optionally specify TYPE. @@ -639,6 +637,8 @@ UPDATE means we are updating, so skip some things." (substitute-command-keys "You have filtered notifications. \ \\[mastodon-notifications-requests] to view requests.\n\n"))) + ;; set update point: + (setq mastodon-tl--update-point (point)) ;; render: (mastodon-notifications--render json (not mastodon-group-notifications)) diff --git a/lisp/mastodon-tl.el b/lisp/mastodon-tl.el index 5b87da235c..508332b098 100644 --- a/lisp/mastodon-tl.el +++ b/lisp/mastodon-tl.el @@ -94,7 +94,7 @@ (autoload 'mastodon-search-load-link-posts "mastodon-search") (autoload 'mastodon-notifications--current-type "mastodon-notifications") (autoload 'mastodon-notifications--timeline "mastodon-notifications") - +(autoload 'mastodon-notifications--empty-group-json-p "mastodon-notifications") (defvar mastodon-toot--visibility) (defvar mastodon-toot-mode) (defvar mastodon-active-user) @@ -110,6 +110,7 @@ (defvar mastodon-media--enable-image-caching) (defvar mastodon-media--generic-broken-image-data) (defvar mastodon-media--sensitive-image-data) +(defvar mastodon-media--attachments) ;;; CUSTOMIZES @@ -171,7 +172,7 @@ nil." (status . ("✍" . "[posted]")) (replied . ("⬇" . "↓")) (reply-bar . ("┃" . "|")) - (poll . ("📊" . "")) + (poll . ("📊" . "[poll]")) (follow . ("👤" . "+")) (follow_request . ("👤" . "+")) (severed_relationships . ("🔗" . "//")) @@ -333,6 +334,22 @@ types of mastodon links and not just shr.el-generated ones.") "The keymap to be set for the author byline. It is active where point is placed by `mastodon-tl-goto-next-item.'") +(require 'image-mode) + +(defvar mastodon-image-mode-map + (let ((map (make-sparse-keymap))) + (set-keymap-parent map image-mode-map) + (define-key map (kbd ">") #'mastodon-tl-next-full-image) + (define-key map (kbd "<") #'mastodon-tl-prev-full-image) + (define-key map (kbd ".") #'mastodon-tl-next-full-image) + (define-key map (kbd ",") #'mastodon-tl-prev-full-image) + ;; matches view full image binding in main keymap: + (define-key map (kbd "=") #'mastodon-tl-next-full-image) + (define-key map (kbd "-") #'mastodon-tl-prev-full-image) + (define-key map (kbd "<right>") #'mastodon-tl-next-full-image) + (define-key map (kbd "<left>") #'mastodon-tl-prev-full-image) + map)) + ;;; MACROS @@ -750,8 +767,10 @@ The result is added as an attachments property to author-byline." (let ((media (mastodon-tl--field 'media_attachments toot))) (mapcar (lambda (attachment) (let-alist attachment - (list :url (or .remote_url .url) ; fallback for notifications - :type .type))) + (list :id .id + :type .type + ;; fallback for notifications: + :url (or .remote_url .url)))) media))) (defun mastodon-tl--byline-booster (toot) @@ -1104,24 +1123,30 @@ Return nil if no matching element." (setq mention (pop mentions))) return)))) -(defun mastodon-tl--userhandle-from-url (url buffer-text) +(defun mastodon-tl--userhandle-from-url (url &optional buffer-text) "Return the user hande the URL points to or nil if it is not a profile link. BUFFER-TEXT is the text covered by the link with URL, for a user profile -this should be of the form <at-sign><user id>, e.g. \"@Gargon\"." +this should be of the form <at-sign><user id>, e.g. \"@Gargon\". +This is called on all post URLs, so needs to handle non profile URLs +gracefully." (let* ((parsed-url (url-generic-parse-url url)) (host (url-host parsed-url)) (local-p (string= (url-host (url-generic-parse-url mastodon-instance-url)) host)) - (path (url-filename parsed-url))) - (when (and (string= "@" (substring buffer-text 0 1)) - ;; don't error on domain only url (rare): - (not (string= "" path)) - (string= (downcase buffer-text) - (downcase (substring path 1)))) - (if local-p - buffer-text ; no instance suffix for local mention - (concat buffer-text "@" host))))) + (path-raw (url-filename parsed-url))) + (unless (string-empty-p path-raw) + (let ((path (substring path-raw 1))) ;; remove "/" prefix + (if (not buffer-text) + (when (string-prefix-p "@" path) + (if local-p path (concat "@" host))) + (when (and (string= "@" (substring buffer-text 0 1)) + ;; don't error on domain only url (rare): + (string= (downcase buffer-text) + (downcase path))) + (if local-p + buffer-text ; no instance suffix for local mention + (concat buffer-text "@" host)))))))) (defun mastodon-tl--hashtag-from-url (url instance-url) "Return the hashtag that URL points to or nil if URL is not a tag link. @@ -1399,18 +1424,103 @@ SENSITIVE is a flag from the item's JSON data." help-echo (concat help-echo "\nC-RET: play " type " with mpv")))) -(defun mastodon-tl-view-full-image () +;;; FULL IMAGE VIEW + +(define-derived-mode mastodon-image-mode image-mode + "mastodon-image" + :group 'mastodon) + +;; patch `shr-browse-image' to accept url arg: +(defun mastodon-tl-shr-browse-image (&optional image-url copy-url) + "Browse the image under point. +If COPY-URL (the prefix if called interactively) is non-nil, copy +the URL of the image to the kill buffer instead. +Optionally use IMAGE-URL rather than the image-url property at point." + (interactive "sP") + (let ((url (or image-url (get-text-property (point) 'image-url)))) + (cond + ((not url) + (message "No image under point")) + (copy-url + (with-temp-buffer + (insert url) + (copy-region-as-kill (point-min) (point-max)) + (message "Copied %s" url))) + (t + (message "Browsing %s..." url) + (browse-url url))))) + +(defun mastodon-tl--view-image-url (url attachments) + "View image URL. Set ATTACHMENTS metadata in image buffer." + (if (not url) + (user-error "No url found") + (if (not mastodon-tl--load-full-sized-images-in-emacs) + (mastodon-tl-shr-browse-image url) + (mastodon-media--image-or-cached + url #'mastodon-media--process-full-sized-image-response + `(nil ,url ,attachments ,(buffer-name)))))) + +(defun mastodon-tl-view-full-image-at-point () "Browse full-sized version of image at point in a new window." (interactive) (if (not (eq (mastodon-tl--property 'mastodon-tab-stop) 'image)) (user-error "No image at point?") - (let* ((url (mastodon-tl--property 'image-url))) - (if (not mastodon-tl--load-full-sized-images-in-emacs) - (shr-browse-image) - (mastodon-media--image-or-cached - url - #'mastodon-media--process-full-sized-image-response - `(nil ,url)))))) + (let* ((url (mastodon-tl--property 'image-url)) + (attachments (mastodon-tl--property 'attachments))) + (mastodon-tl--view-image-url url attachments)))) + +(defun mastodon-tl-view-first-full-image () + "From item byline, fetch load its first full image." + (interactive) + (let* ((attachments (mastodon-tl--property 'attachments)) + (url (plist-get (car attachments) :url))) + (if (not attachments) + (user-error "Toot has no attachments") + (mastodon-tl--view-image-url url attachments)))) + +(defun mastodon-tl--get-next-image-url () + "Return the url for the next image to load. +Cycles through values in `mastodon-media--attachments'." + (let* ((url (car mastodon-media--attachments)) + ;; match url against our plists: + (current (mastodon-tl--current-image-url url))) + ;; fetch from next item in current or use first item if current has + ;; only 1 item: + (plist-get (if (= 1 (length current)) + (cadr mastodon-media--attachments) + (cadr current)) + :url))) + +(defun mastodon-tl--current-image-url (url) + "Try to fetch URL from `mastodon-media--attachments'. +The return value is that of `cl-member-if', ie if a match is found, it +returns the match and the list of which it is the car." + (cl-member-if + (lambda (attachment) + (equal url (plist-get attachment :url))) + (cdr mastodon-media--attachments))) + +(defun mastodon-tl--get-prev-image-url () + "Return the URL of the previous item in `mastodon-media--attachments'." + (let* ((url (car mastodon-media--attachments)) + (current (mastodon-tl--current-image-url url))) + (plist-get (nth (1- (length current)) + (cdr mastodon-media--attachments)) + :url))) + +(defun mastodon-tl-next-full-image () + "From full image view buffer, load the toot's next image." + (interactive) + (let* ((next-url (mastodon-tl--get-next-image-url))) + (mastodon-tl--view-image-url next-url + (cdr mastodon-media--attachments)))) + +(defun mastodon-tl-prev-full-image () + "From full image view buffer, load the toot's prev image." + (interactive) + (let* ((prev-url (mastodon-tl--get-prev-image-url))) + (mastodon-tl--view-image-url prev-url + (cdr mastodon-media--attachments)))) (defun mastodon-tl-toggle-sensitive-image () "Toggle dislay of sensitive image at point." @@ -1455,7 +1565,9 @@ LENGTH is of the longest option, for formatting." (let* ((options (mastodon-tl--map-alist 'title .options)) (longest (car (sort (mapcar #'length options ) #'>))) (counter 0)) - (concat "\nPoll: \n\n" + (concat "\n" + (mastodon-tl--symbol 'poll) + "\n\n" (mapconcat (lambda (option) (setq counter (1+ counter)) (mastodon-tl--format-poll-option @@ -1612,7 +1724,7 @@ OPTIONS is an alist." (interactive "d") (if (mastodon-tl--media-video-p) (mastodon-tl-mpv-play-video-at-point) - (mastodon-tl-view-full-image))) + (mastodon-tl-view-full-image-at-point))) (defun mastodon-tl-click-image-or-video (event) "Click to play video with `mpv.el'. @@ -2180,6 +2292,18 @@ call this function after it is set or use something else." This includes the update profile note buffer, but not the preferences one." (string-prefix-p "accounts" (mastodon-tl--endpoint nil :no-error))) +(defun mastodon-tl--own-profile-buffer-p () + "Return t if we are viewing our own profile buffer. +We check that our account credientials id matches the endpoint id in the +buffer spec, which if in a profile buffer is of the form +\"accounts/$id/statuses\"." + (and (mastodon-tl--profile-buffer-p) + (let ((endpoint-id + (nth 1 + (split-string (mastodon-tl--endpoint) "/")))) + (string= (mastodon-auth--get-account-id) + endpoint-id)))) + (defun mastodon-tl--search-buffer-p () "T if current buffer is a search buffer." (string-suffix-p "search" (mastodon-tl--endpoint nil :no-error))) @@ -2491,7 +2615,7 @@ ID is that of the post the context is currently displayed for." ;;; FOLLOW/BLOCK/MUTE, ETC (defun mastodon-tl-follow-user (user-handle - &optional notify langs reblogs json) + &optional notify langs reblogs json) "Query for USER-HANDLE from current status and follow that user. If NOTIFY is \"true\", enable notifications when that user posts. If NOTIFY is \"false\", disable notifications when that user posts. @@ -2505,6 +2629,32 @@ JSON is a flag arg for `mastodon-http--post'." (mastodon-tl--do-user-action-and-response user-handle "follow" nil notify langs reblogs json))) +(defun mastodon-tl-follow-user-by-handle (user-handle) + "Prompt for a USER-HANDLE and follow that user. +USER-HANDLE can also be a URL to a user profile page." + ;; code adapted from sachac: + ;; https://sachachua.com/dotemacs/index.html#mastodon. thanks sachac! + (interactive "MHandle: ") + (when (string-match "https?://\\(.+?\\)/\\(@.+\\)" user-handle) + (setq user-handle + ;; sachac's model doesn't work with local user handles in URL, + ;; meaning the search below will fail, so we use our own + ;; URL-to-handle function, modified for the purpose: + ;; (concat (match-string 2 user-handle) "@" (match-string 1 user-handle)))) + (mastodon-tl--userhandle-from-url user-handle))) + (let* ((account (mastodon-profile--search-account-by-handle + user-handle)) + (user-id (alist-get 'id account)) + (name (if (not (string-empty-p + (alist-get 'display_name account))) + (alist-get 'display_name account) + (alist-get 'username account))) + (url (mastodon-http--api (format "accounts/%s/%s" user-id "follow")))) + (if account + (mastodon-tl--do-user-action-function url name + (substring user-handle 1) "follow") + (user-error "Cannot find a user with handle %S" user-handle)))) + ;; TODO: make this action "enable/disable notifications" (defun mastodon-tl-enable-notify-user-posts (user-handle) "Query for USER-HANDLE and enable notifications when they post." @@ -2974,6 +3124,13 @@ report the account for spam." ;;; UPDATING, etc. +(defun mastodon-tl--no-json (json) + "Nil if JSON is nil or empty group notif data." + (if (and (mastodon-tl--buffer-type-eq 'notifications) + mastodon-group-notifications) + (mastodon-notifications--empty-group-json-p json) + (not json))) + (defun mastodon-tl--more-json (endpoint id) "Return JSON for timeline ENDPOINT before ID." (let* ((args `(("max_id" . ,(mastodon-tl--as-string id)))) @@ -3021,13 +3178,14 @@ Then run CALLBACK with arguments CBARGS." (setf (alist-get "offset" params nil nil #'string=) offset) (apply #'mastodon-http--get-json-async url params callback cbargs))) -(defun mastodon-tl--updated-json (endpoint id &optional params) +(defun mastodon-tl--updated-json (endpoint id &optional params version) "Return JSON for timeline ENDPOINT since ID. PARAMS is used to send any parameters needed to correctly update -the current view." +the current view. +VERSION is the API version to use, as grouped notifs use v2." (let* ((args `(("since_id" . ,(mastodon-tl--as-string id)))) (args (append args params)) - (url (mastodon-http--api endpoint))) + (url (mastodon-http--api endpoint version))) (mastodon-http--get-json url args))) ;; TODO: add this to new posts in some cases, e.g. in thread view. @@ -3169,7 +3327,7 @@ MAX-ID is the pagination parameter, a string." (mastodon-tl--thread-do) (goto-char point-before) (message "Loaded full thread.")) - (if (not json) + (if (mastodon-tl--no-json json) (user-error "No more results") (if notifs-p (mastodon-notifications--timeline json notif-type :update) @@ -3361,27 +3519,34 @@ This location is defined by a non-nil value of (user-error "Update not available in this view") ;; FIXME: handle update for search and trending buffers (let* ((endpoint (mastodon-tl--endpoint)) - (update-function (mastodon-tl--update-function)) + (update-fun (mastodon-tl--update-function)) (id (mastodon-tl--newest-id))) ;; update a thread, without calling `mastodon-tl--updated-json': (if (mastodon-tl--buffer-type-eq 'thread) ;; load whole thread: (progn (mastodon-tl--thread-do id) (message "Loaded full thread.")) - ;; update other timelines: - (let* ((params (mastodon-tl--update-params)) - (json (mastodon-tl--updated-json endpoint id params))) - (if (not json) - (user-error "Nothing to update") - (let ((inhibit-read-only t)) - (mastodon-tl--set-after-update-marker) - (goto-char (or mastodon-tl--update-point (point-min))) - (if (eq update-function 'mastodon-notifications--timeline) - (funcall update-function json nil :update) - (funcall update-function json)) - (if mastodon-tl--after-update-marker - (goto-char mastodon-tl--after-update-marker) - (mastodon-tl-goto-next-item))))))))) + (if (not id) ;; if e.g. notifs all cleared: + (user-error "No last id") + ;; update other timelines: + (let* ((params (mastodon-tl--update-params)) + (notifs-p + (eq update-fun 'mastodon-notifications--timeline)) + (json (mastodon-tl--updated-json + endpoint id params + (when (and notifs-p mastodon-group-notifications) + "v2")))) + (if (mastodon-tl--no-json json) + (user-error "Nothing to update") + (let ((inhibit-read-only t)) + (mastodon-tl--set-after-update-marker) + (goto-char (or mastodon-tl--update-point (point-min))) + (if notifs-p + (funcall update-fun json nil :update) + (funcall update-fun json)) + (if mastodon-tl--after-update-marker + (goto-char mastodon-tl--after-update-marker) + (mastodon-tl-goto-next-item)))))))))) ;;; LOADING TIMELINES diff --git a/lisp/mastodon-toot.el b/lisp/mastodon-toot.el index 83c4d9be36..9ebb1ef0c7 100644 --- a/lisp/mastodon-toot.el +++ b/lisp/mastodon-toot.el @@ -106,6 +106,7 @@ (autoload 'mastodon-tl--image-trans-check "mastodon-tl") (autoload 'mastodon-instance-data "mastodon") (autoload 'mastodon-create-poll "mastodon-transient") +(autoload 'mastodon-tl--own-profile-buffer-p "mastodon-tl") ;; for mastodon-toot-translate-toot-text (autoload 'mastodon-tl--content "mastodon-tl") @@ -647,11 +648,13 @@ Uses `lingva.el'." (if (not pinnable-p) (user-error "You can only pin your own toots") (when (y-or-n-p (format "%s this toot? " (capitalize action))) - (mastodon-toot--action action - (lambda (_) - (when mastodon-tl--buffer-spec - (mastodon-tl--reload-timeline-or-profile)) - (message "Toot %s!" msg))))))) + (mastodon-toot--action + action + (lambda (_) + ;; let's only reload when in own profile view: + (when (mastodon-tl--own-profile-buffer-p) + (mastodon-tl--reload-timeline-or-profile)) + (message "Toot %s!" msg))))))) ;;; DELETE, DRAFT, REDRAFT diff --git a/lisp/mastodon.el b/lisp/mastodon.el index aefdc67786..1e973d78de 100644 --- a/lisp/mastodon.el +++ b/lisp/mastodon.el @@ -6,8 +6,8 @@ ;; Author: Johnson Denen <johnson.de...@gmail.com> ;; Marty Hiatt <mouse...@disroot.org> ;; Maintainer: Marty Hiatt <mouse...@disroot.org> -;; Version: 1.1.12 -;; Package-Requires: ((emacs "28.1") (request "0.3.0") (persist "0.4") (tp "0.7")) +;; Version: 2.0.0 +;; Package-Requires: ((emacs "28.1") (persist "0.4") (tp "0.7")) ;; Homepage: https://codeberg.org/martianh/mastodon.el ;; This file is not part of GNU Emacs. @@ -105,7 +105,12 @@ (defvar mastodon-tl--highlight-current-toot) (defvar mastodon-notifications--map) -(defvar mastodon-notifications-grouped-types) +(defvar mastodon-client--token-file) + +(defvar mastodon-notifications-grouped-types + '("reblog" "favourite") ;; TODO: implement follow! + "List of notification types for which grouping is implemented. +Used in `mastodon-notifications-get'") (defgroup mastodon nil "Interface with Mastodon." @@ -191,6 +196,23 @@ and X others...\"." (interactive) (quit-window 'kill)) +(defvar mastodon-client--active-user-details-plist) +(defvar mastodon-auth--token-alist) + +;;;###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 @@ -236,6 +258,7 @@ and X others...\"." (define-key map (kbd "T") #'mastodon-tl-thread) (define-key map (kbd "RET") #'mastodon-tl-thread) (define-key map (kbd "m") #'mastodon-tl-dm-user) + (define-key map (kbd "=") #'mastodon-tl-view-first-full-image) (when (require 'lingva nil :no-error) (define-key map (kbd "a") #'mastodon-toot-translate-toot-text)) (define-key map (kbd ",") #'mastodon-toot-list-favouriters) diff --git a/mastodon-index.org b/mastodon-index.org index 7f0023ae5c..b10b4a45e1 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. | @@ -150,12 +152,15 @@ | | mastodon-tl-mpv-play-video-from-byline | Run `mastodon-tl-mpv-play-video-at-point' on first moving image in post. | | | mastodon-tl-mute-thread | Mute the thread displayed in the current buffer. | | M | mastodon-tl-mute-user | Query for USER-HANDLE from current status and mute that user. | +| | mastodon-tl-next-full-image | From full image view buffer, load the toot's next image. | | TAB, M-n | mastodon-tl-next-tab-item | Move to the next interesting item. | | v | mastodon-tl-poll-vote | If there is a poll at point, prompt user for OPTION to vote on it. | +| | mastodon-tl-prev-full-image | From full image view buffer, load the toot's prev image. | | S-TAB, <backtab> | mastodon-tl-previous-tab-item | Move to the previous interesting item. | | | mastodon-tl-remote-tag-timeline | Call `mastodon-tl-get-remote-local-timeline' but for a TAG timeline. | | Z | mastodon-tl-report-to-mods | Report the author of the toot at point to your instance moderators. | | SPC | mastodon-tl-scroll-up-command | Call `scroll-up-command', loading more toots if necessary. | +| | mastodon-tl-shr-browse-image | Browse the image under point. | | | mastodon-tl-single-toot | View toot at point in separate buffer. | | | mastodon-tl-some-followed-tags-timeline | Prompt for some tags, and open a timeline for them. | | C-' | mastodon-tl-tag-group-timeline | Load a timeline of a tag group from `mastodon-tl--tags-groups'. | @@ -172,7 +177,8 @@ | | mastodon-tl-unmute-thread | Unmute the thread displayed in the current buffer. | | S-RET | mastodon-tl-unmute-user | Query for USER-HANDLE from list of muted users and unmute that user. | | u, g | mastodon-tl-update | Update timeline with new toots. | -| | mastodon-tl-view-full-image | Browse full-sized version of image at point in a new window. | +| = | mastodon-tl-view-first-full-image | From item byline, fetch load its first full image. | +| | mastodon-tl-view-full-image-at-point | Browse full-sized version of image at point in a new window. | | | mastodon-tl-view-full-image-or-play-video | View full sized version of image at point, or try to play video. | | | mastodon-tl-view-item-on-own-instance | Load current toot on your own instance. | | | mastodon-tl-view-whole-thread | From a thread view, view entire thread. | @@ -274,53 +280,54 @@ #+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-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"))) diff --git a/test/mastodon-http-tests.el b/test/mastodon-http-tests.el index 9e0b413909..4bef0eefe7 100644 --- a/test/mastodon-http-tests.el +++ b/test/mastodon-http-tests.el @@ -61,28 +61,32 @@ Strict-Transport-Security: max-age=31536000 "Should run success function for 200 HTML response." (let ((response-buffer (get-buffer-create "mastodon-http--triage-buffer"))) - (with-current-buffer response-buffer - (erase-buffer) - (insert mastodon-http--example-200)) - (should (equal (mastodon-http--triage - response-buffer - (lambda (_) - (message "success call"))) - "success call")))) + (with-mock + (mock (url-http-parse-response) => 200) + (with-current-buffer response-buffer + (erase-buffer) + (insert mastodon-http--example-200)) + (should (equal (mastodon-http--triage + response-buffer + (lambda (_) + (message "success call"))) + "success call"))))) (ert-deftest mastodon-http--triage-failure () "Should return formatted JSON error from bad HTML response buffer. Should not run success function." (let ((response-buffer (get-buffer-create "mastodon-http--triage-buffer"))) - (with-current-buffer response-buffer - (erase-buffer) - (insert mastodon-http--example-400)) - (should (equal (mastodon-http--triage - response-buffer - (lambda (_) - (message "success call"))) - "Error 444: some unhappy complaint")))) + (with-mock + (mock (url-http-parse-response) => 444) + (with-current-buffer response-buffer + (erase-buffer) + (insert mastodon-http--example-400)) + (should (equal (mastodon-http--triage + response-buffer + (lambda (_) + (message "success call"))) + "Error 444: some unhappy complaint"))))) (ert-deftest mastodon-http-params-build () "Should correctly format parameters from an alist." diff --git a/test/mastodon-toot-tests.el b/test/mastodon-toot-tests.el index b01c36fca5..7d5da284e6 100644 --- a/test/mastodon-toot-tests.el +++ b/test/mastodon-toot-tests.el @@ -209,6 +209,7 @@ mention string." (toot mastodon-toot-test-base-toot) (id 61208)) (with-mock + (mock (url-http-parse-response) => 200) (mock (mastodon-tl--property 'base-item-id) => id) (mock (mastodon-http--api "statuses/61208/pin") => "https://example.space/statuses/61208/pin")