appwrite.el/appwrite.el

438 lines
18 KiB
EmacsLisp
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; appwrite.el --- Appwrite server SDK -*- lexical-binding: t; -*-
;; Copyright (C) 2022 Lucien Cartier-Tilet
;; Author: Lucien Cartier-Tilet <lucien@phundrak.com>
;; Maintainer: Lucien Cartier-Tilet <lucien@phundrak.com>
;; URL: https://github.com/Phundrak/appwrite.el
;; Version: 0.1.0
;; Package-Requires: ((emacs "27.1"))
;; Keywords: extensions, lisp, database, appwrite, tools
;; This file is not part of GNU Emacs.
;; This program 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.
;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Appwrite server SDK for Emacs
;;
;;; Code:
(require 'cl-lib)
(require 'json)
(require 'url)
(defgroup appwrite nil
"Customizationi group for `appwrite'."
:group 'emacs-lisp
:link '(url-link :tag "Gitea" "https://labs.phundrak.com/phundrak/appwrite.el")
:link '(url-link :tag "Github" "https://github.com/Phundrak/appwrite.el"))
(defcustom appwrite-endpoint ""
"Appwrite endpoint.
Must not include the API version, e.g.
\"https://appwrite.example.org\". The variable must not end with
a trailing forward slash. Setting this variable with
`customize-set-variable' takes care of it automatically."
:group 'appwrite
:type 'string
:set (lambda (symbol val)
(set-default symbol
(if (string-suffix-p "/" val)
(message "%s" (substring val 0 (1- (length val))))
val))))
(defvar appwrite-api-key ""
"API Key for accessing your Appwrite API.
Be sure to keep it safe and never upload its value somewhere on
the internet, even if its a private repository.")
(defvar appwrite-project ""
"ID of the project to act on.")
;;; Internal functions
(defun appwrite--plistp (object)
"Non-nil if and only if OBJECT is a valid plist.
Compatibility function for Emacs 27 and earlier, the code source
in the else branch is the definition of `plistp' in Emacs 29."
(if (boundp #'plistp)
(plistp object)
(let ((len (proper-list-p object)))
(and len (zerop (% len 2))))))
(defun appwrite--get-full-url (api)
"Get the full url for API.
If API does not begin with an initial forward slash, add it
automatically. If it does not contain an API version, prefix
\"/v1\" by default."
(let ((versionp (string-match-p "^/?v[[:digit:]]+.*" api))
(initial-forward-slash-p (string-prefix-p "/" api)))
(concat appwrite-endpoint
(if versionp
""
"/v1")
(if initial-forward-slash-p
""
"/")
api)))
(defun appwrite--message-failure (message status json-message)
"Display MESSAGE followed by JSON-MESSAGE.
This will show a message in the modeline in this format:
[status STATUS] MESSAGE: JSON-MESSAGE"
(message "[status %d] %s: %s" status message json-message))
(defun appwrite--process-response (message success-status response)
"In case of failure when calling the Appwrite API, display MESSAGE.
The function considers a call to the API a failure in case the
HTTP status code in RESPONSE differs from SUCCESS-STATUS, the
HTTP status code hoped for. If thats the case, warn the user,
see `appwrite--message-failure', else return the JSON returned by
the API."
(let ((status (car response))
(json (cdr response)))
(if (= status success-status)
json
(appwrite--message-failure message status (gethash "message" json)))))
(cl-defun appwrite--query-api (&key (method "GET")
api
payload
(content-type "application/json")
payload-alist-p
asyncp
callback
extra-headers)
"Perform a method METHOD to API with PAYLOAD as its payload.
CONTENT-TYPE is whichever miime-type is being used.
If CONTENT-TYPE is \"application/json\", PAYLOAD is subject ot
automatic conversion depending on its type.
- If PAYLOAD passes `appwrite--plistp', it will be converted to
JSON as a plist.
- If PAYLOAD passes `hash-table-p', it will be converted to JSON
as a hash table.
- If PAYLOAD-ALIST-P is t, PAYLOAD will be converted to JSON as an
associative table.
- Else, PAYLOAD will be passed a string containing JSON.
If ASYNCP is t, `appwrite--post-api' will be asynchronous.
CALLBACK must then be set as it will be called once the request
finishes. See `url-retrieve'.
EXTRA-HEADERS is a list of pairs to append to
`url-request-extra-headers'.
The function returns a pair composed of the HTTP status code as
its car. The cdr is a hash table from the response answer if
Content-Type in the headers is \"application/json\"."
(let* ((url (appwrite--get-full-url api))
(url-request-method method)
(url-request-extra-headers `(("X-Appwrite-key" . ,appwrite-api-key)
("X-Appwrite-Project" . ,appwrite-project)
("Content-type" . ,content-type)))
(url-request-extra-headers (if extra-headers
(append url-request-extra-headers
extra-headers)
url-request-extra-headers))
(url-request-data (cond ((not (string= content-type "application/json"))
payload)
((appwrite--plistp payload)
(json-encode-plist payload))
((hash-table-p payload) (json-encode payload))
(payload-alist-p (json-encode-alist payload))
(t payload))))
(if asyncp
(url-retrieve url callback)
(with-current-buffer (url-retrieve-synchronously url)
(let (http-code json)
(message ";;;;;;;;;;;;")
(message "%s" (buffer-string))
(save-match-data
(goto-char (point-min))
(re-search-forward (rx bol "HTTP" (+ (not space)) " " (group (+ digit))))
(setq http-code (string-to-number
(buffer-substring-no-properties (match-beginning 1)
(match-end 1)))))
(when (re-search-forward "^Content-Type: application/json" nil t)
(goto-char (point-min))
(re-search-forward "^$")
(delete-region (point) (point-min))
(setq json (json-parse-buffer)))
`(,http-code . ,json))))))
;;; Account
;;; Users
;;; Teams
;;; Databases
;;; Storage
(cl-defun appwrite--storage-update-bucket (id
name
&key
updatep
(permission "bucket")
read write (enabled t)
maximum-file-size allowed-file-extensions
(encryption t) (antivirus t))
"Create or update a storage bucket.
Create or update a storage bucket named NAME with id ID.
If UPDATEP is t, update the bucket, else create it.
PERMISSION is the permissioin type model to use for reading files
in this bucket. By default, PERMISSION is \"bucket\". For more
info, see https://appwrite.io/docs/permissions
READ is an array of roles for read permissions.
WRITE is an array of roles for write permissions.
If ENABLED is nil, disable bucket, t by default.
MAXIMUM-FILE-SIZE is an integer indicating in bytes the maximum
size of an uploaded. The default is 30MB (not MiB), though this
might be different on self-hosted instances.
ALLOWED-FILE-EXTENSIONS is an array of allowed file extensions. A
maximum of 100 extensions no longer than 64 characters are
allowed.
If ENCRYPTION is t, enable encryption for the bucket. Files
larger than 20MB are skipped. t by default.
If ANTIVIRUS is t, enable antivirus for the bucket. Files larger
than 20MB are skipped. t by default."
(let ((payload `(bucketId ,id name ,name permission ,permission))
(method (if updatep "PUT" "POST")))
(when read
(setq payload (append payload `(read ,read))))
(when write
(setq payload (append payload `(write ,write))))
(when maximum-file-size
(setq payload (append payload `(maximumFileSize ,maximum-file-size))))
(when allowed-file-extensions
(setq payload (append payload `(allowedFileExtensions ,allowed-file-extensions))))
(setq payload (append payload `(enabled ,(if enabled t :json-false))))
(setq payload (append payload `(encryption ,(if encryption t :json-false))))
(setq payload (append payload `(antivirus ,(if antivirus t :json-false))))
;; (json-encode-plist payload)
(let ((response (appwrite--query-api :method method
:api (concat "/v1/storage/buckets/"
(if updatep id ""))
:payload (json-encode-plist payload))))
(appwrite--process-response (format "Failed to %s bucket %s"
(if updatep "update" "create")
id)
(if updatep 200 201)
response))))
(cl-defun appwrite-storage-create-bucket (id
name
&key
(permission "bucket")
read write (enabled t)
maximum-file-size allowed-file-extensions
(encryption t) (antivirus t))
"Create storage bucket.
For documentation on ID, NAME, PERMISSION, READ, WRITE, ENABLED,
MAXIMUM-FILE-SIZE, ALLOWED-FILE-EXTENSIONS, ENCRYPTION, and
ANTIVIRUS, check `appwrite--storage-update-bucket'."
(appwrite--storage-update-bucket id
name
:updatep nil
:permission permission
:read read
:write write
:enabled enabled
:maximum-file-size maximum-file-size
:allowed-file-extensions allowed-file-extensions
:encryption encryption
:antivirus antivirus))
(cl-defun appwrite-storage-update-bucket (id
name
&key
(permission "bucket")
read write (enabled t)
maximum-file-size allowed-file-extensions
(encryption t) (antivirus t))
"Create storage bucket.
For documentation on ID, NAME, PERMISSION, READ, WRITE, ENABLED,
MAXIMUM-FILE-SIZE, ALLOWED-FILE-EXTENSIONS, ENCRYPTION, and
ANTIVIRUS, check `appwrite--storage-update-bucket'."
(appwrite--storage-update-bucket id
name
:updatep t
:permission permission
:read read
:write write
:enabled enabled
:maximum-file-size maximum-file-size
:allowed-file-extensions allowed-file-extensions
:encryption encryption
:antivirus antivirus))
(cl-defun appwrite-storage-list-buckets (&key search limit offset cursor cursor-direction order-type)
"List of all storage buckets.
SEARCH is a string to filter the list results when non-nil. Max
length of 256 chars.
LIMIT is the maximum amount of buckets returned by the
query. Appwrite defaults at 25.
OFFSET is the results offset with which the user can manage the
pagination of the results when non-nil. Appwrite defaults at 0.
CURSOR is the id of the bucket used as the starting point of the
query, excluding the bucket itself.
CURSOR-DIRECTION can be either \\='after or \\='before.
ORDER-TYPE can be either \\='ascending or \\='descending.
If the query is successful, return a hash table made from the
acquired JSON. Otherwise, return nil and warn the user."
(let (payload)
(when search (setq payload (append payload `(search ,search))))
(setq payload (append payload `(limit ,limit)))
(when offset (setq payload (append payload `(offset ,offset))))
(when cursor (setq payload (append payload `(cursor ,cursor))))
(when-let ((direction (pcase cursor-direction
('before "before")
('after "after")
(_ nil))))
(setq payload (append payload `(cursorDirection ,direction))))
(when-let ((order (pcase order-type
('ascending "ASC")
('descending "DESC")
(_ nil))))
(setq payload (append payload `(orderType ,order))))
(let ((response (appwrite--query-api :api "/v1/storage/buckets"
:payload (json-encode-plist payload))))
(appwrite--process-response "Failed to list buckets" 200 response))))
(defun appwrite-storage-get-bucket (id)
"Get bucket with id ID."
(let ((response (appwrite--query-api :api (concat "/v1/storage/buckets/" id))))
(appwrite--process-response (concat "Failed to get bucket " id)
200
response)))
(defun appwrite-storage-delete-bucket (id)
"Delete bucket with id ID."
(let ((response (appwrite--query-api :method "DELETE"
:api (concat "/v1/storage/buckets/" id))))
(appwrite--process-response (concat "Failed to delete bucket " id)
204
response)))
(cl-defun appwrite-storage-create-file (bucket-id file-id file &key read write)
"Upload FILE in BUCKET-ID as FILE-ID.
Additionally give the file extra READ or WRITE user permissions.
TODO: implement file upload."
(let ((file (expand-file-name file)))
(unless (file-exists-p file)
(error "File does not exist: %s" file)
(let ((payload `(bucketId ,bucket-id fileId ,file-id file ,file)))
(when read (setq payload (append payload `(read ,read))))
(when write (setq payload (append payload `(write ,write))))
(json-encode-plist payload))))
(warn "The file upload part of `appwrite-storage-create-file' hasn't been implemented yet"))
(cl-defun appwrite-storage-list-files (id &key search limit offset cursor cursor-direction order-type)
"List files in storage bucket ID.
SEARCH is a term that can filter the list results. Max length:
256 chars.
LIMIT is the maximum number of files to return. Appwrite
defaults to 25.
OFFSET manages pagination in the search result. Appwrite
defaults to 0.
CURSOR is the ID of the file used as the starting point for the
query, excluding the file itself.
CURSOR-DIRECTION can be either \\='after or \\='before.
ORDER-TYPE can be either \\='ascending or \\='descending.
If the query is successful, return a hash table made from the
acquired JSON. Otherwise, return nil and warn the user."
(let ((payload `(bucketId ,id)))
(when search (setq payload `(search ,search)))
(when limit (setq payload `(limit ,limit)))
(when offset (setq payload `(offset ,offset)))
(when cursor (setq payload `(cursor ,cursor)))
(when-let ((direction (pcase cursor-direction
('before "before")
('after "after")
(_ nil))))
(setq payload (append payload `(cursorDirection ,direction))))
(when-let ((order (pcase order-type
('ascending "ASC")
('descending "DESC")
(_ nil))))
(setq payload (append payload `(orderType ,order))))
(let ((response (appwrite--query-api :api (format "/v1/storage/buckets/%s/files"
id)
:payload (json-encode-plist payload))))
(appwrite--process-response "Failed to list files" 200 response))))
(defun appwrite-storage-get-file (bucket-id file-id)
"Retrieve a file object from a bucket.
The bucket is determined by its id BUCKET-ID while the file is
determined by its id FILE-ID.
This does not download the file! For that, see
`appwrite-storage-get-file-download'."
(let ((payload `(bucketId ,bucket-id fileId ,file-id)))
(appwrite--query-api :api (format "/v1/storage/buckets/%s/files/%s"
bucket-id
file-id)
:payload (json-encode-plist payload))))
;;; Functions
;;; Localization
;;; Avatars
;;; Health
(provide 'appwrite)
;;; appwrite.el ends here