;;; mastodon-client.el --- Client functions for mastodon.el  -*- lexical-binding: t -*-

;; Copyright (C) 2017-2019 Johnson Denen
;; Copyright (C) 2021 Abhiseck Paira <abhiseckpaira@disroot.org>
;; Copyright (C) 2025 Marty Hiatt <martianh@disroot.org>
;; Author: Johnson Denen <johnson.denen@gmail.com>
;; Maintainer: Marty Hiatt <martianh@disroot.org>
;; Homepage: https://codeberg.org/martianh/mastodon.el

;; This file is not part of GNU Emacs.

;; This file is part of mastodon.el.

;; mastodon.el is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; mastodon.el is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with mastodon.el.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; mastodon-client.el supports registering the Emacs client with your Mastodon instance.

;;; Code:

(require 'plstore)
(require 'json)
(require 'url)

(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")
  "File path where Mastodon access tokens are stored."
  :group 'mastodon
  :type 'file)

(defvar mastodon-client--client-details-alist nil
  "An alist of Client id and secrets keyed by the instance url.")

(defvar mastodon-client--active-user-details-plist nil
  "A plist of active user details.")

(defvar mastodon-client-scopes "read write follow"
  "Scopes to pass to oauth during registration.")

(defvar mastodon-client-website "https://codeberg.org/martianh/mastodon.el"
  "Website of mastodon.el.")

(defvar mastodon-client-redirect-uri "urn:ietf:wg:oauth:2.0:oob"
  "Redirect_uri as required by oauth.")

(defun mastodon-client--register ()
  "POST client to Mastodon."
  (mastodon-http--post (mastodon-http--api "apps")
                       `(("client_name" . "mastodon.el")
                         ("redirect_uris" . ,mastodon-client-redirect-uri)
                         ("scopes" . ,mastodon-client-scopes)
                         ("website" . ,mastodon-client-website))
                       nil
                       :unauthenticated))

(defun mastodon-client--fetch ()
  "Return JSON from `mastodon-client--register' call."
  (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'."
  mastodon-client--token-file)

(defun mastodon-client--store ()
  "Store client_id and client_secret in `mastodon-client--token-file'.
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)
    (append secrets sans-secrets)))

(defun mastodon-client--remove-key-from-plstore (plstore)
  "Remove KEY from PLSTORE."
  (cdr plstore))

;; Actually it returns a plist with client-details if such details are
;; already stored in mastodon.plstore
(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))))
    (plstore-close plstore)
    (mastodon-client--remove-key-from-plstore mastodon)))

(defun mastodon-client--general-read (key)
  "Retrieve the plstore item keyed by KEY.
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)))

(defun mastodon-client--store-access-token (token)
  "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 (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))
    (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)
    (cdr (plstore-get plstore key))))

(defun mastodon-client--make-user-active (user-details)
  "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)
    (append secrets sans-secrets)))

(defun mastodon-client--form-user-from-vars ()
  "Create a username from user variable.  Return that username.
Username in the form user@instance.com is formed from the
variables `mastodon-instance-url' and `mastodon-active-user'."
  (concat mastodon-active-user
          "@"
          (url-host (url-generic-parse-url mastodon-instance-url))))

(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 (mastodon-client--general-read
                      (concat "user-" username))))
    (when user-plist
      (mastodon-client--make-user-active user-plist)
      ;; user-plist doesn't contain :access_token but i think we ought to
      ;; return a plist containing it here, so that we set
      ;; `mastodon-client--active-user-details-plist' correctly in `mastodon-client--active-user':
      ;; user-plist
      )))

(defun mastodon-client--current-user-active-p ()
  "Return user-details if the current user is active.
Otherwise return nil."
  (let ((username (mastodon-client--form-user-from-vars))
        (user-details (mastodon-client--general-read "active-user")))
    (when (and user-details
               (string= (plist-get user-details :username) username))
      user-details)))

(defun mastodon-client--active-user ()
  "Return the details of the currently active user.
Details is a plist."
  (or mastodon-client--active-user-details-plist
      (setq mastodon-client--active-user-details-plist
            (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'.
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))))
    (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

