2022-07-13 13:56:02 +00:00
|
|
|
|
;;; appwrite.el --- Appwrite server SDK -*- lexical-binding: t; -*-
|
2022-07-13 00:01:17 +00:00
|
|
|
|
|
|
|
|
|
;; 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
|
2022-07-13 13:56:02 +00:00
|
|
|
|
;; Package-Requires: ((emacs "27.1"))
|
|
|
|
|
;; Keywords: extensions, lisp, database, appwrite, tools
|
2022-07-13 00:01:17 +00:00
|
|
|
|
|
|
|
|
|
;; 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.
|
2022-07-13 13:49:50 +00:00
|
|
|
|
\"https://appwrite.example.org\". The variable must not end with
|
|
|
|
|
a trailing forward slash. Setting this variable with
|
2022-07-13 00:01:17 +00:00
|
|
|
|
`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 it’s a private repository.")
|
|
|
|
|
|
|
|
|
|
(defvar appwrite-project ""
|
|
|
|
|
"ID of the project to act on.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Internal functions
|
|
|
|
|
|
2022-07-13 13:43:47 +00:00
|
|
|
|
(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))))))
|
|
|
|
|
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(defun appwrite--get-full-url (api)
|
|
|
|
|
"Get the full url for API.
|
|
|
|
|
If API does not begin with an initial forward slash, add it
|
2022-07-13 13:49:50 +00:00
|
|
|
|
automatically. If it does not contain an API version, prefix
|
|
|
|
|
\"/v1\" by default."
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(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)))
|
|
|
|
|
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(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
|
2022-07-13 13:49:50 +00:00
|
|
|
|
HTTP status code hoped for. If that’s the case, warn the user,
|
|
|
|
|
see `appwrite--message-failure', else return the JSON returned by
|
|
|
|
|
the API."
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(let ((status (car response))
|
|
|
|
|
(json (cdr response)))
|
|
|
|
|
(if (= status success-status)
|
|
|
|
|
json
|
|
|
|
|
(appwrite--message-failure message status (gethash "message" json)))))
|
|
|
|
|
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(cl-defun appwrite--query-api (&key (method "GET")
|
|
|
|
|
api
|
2022-07-13 13:34:50 +00:00
|
|
|
|
payload
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(content-type "application/json")
|
2022-07-13 13:34:50 +00:00
|
|
|
|
payload-alist-p
|
2022-07-13 00:01:17 +00:00
|
|
|
|
asyncp
|
2022-07-13 16:52:58 +00:00
|
|
|
|
callback
|
|
|
|
|
extra-headers)
|
2022-07-13 13:34:50 +00:00
|
|
|
|
"Perform a method METHOD to API with PAYLOAD as its payload.
|
2022-07-13 00:01:17 +00:00
|
|
|
|
CONTENT-TYPE is whichever miime-type is being used.
|
|
|
|
|
|
2022-07-13 13:34:50 +00:00
|
|
|
|
If CONTENT-TYPE is \"application/json\", PAYLOAD is subject ot
|
2022-07-13 00:01:17 +00:00
|
|
|
|
automatic conversion depending on its type.
|
2022-07-13 13:43:47 +00:00
|
|
|
|
- If PAYLOAD passes `appwrite--plistp', it will be converted to
|
|
|
|
|
JSON as a plist.
|
2022-07-13 13:34:50 +00:00
|
|
|
|
- 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
|
2022-07-13 00:01:17 +00:00
|
|
|
|
associative table.
|
2022-07-13 13:34:50 +00:00
|
|
|
|
- Else, PAYLOAD will be passed a string containing JSON.
|
2022-07-13 00:01:17 +00:00
|
|
|
|
|
|
|
|
|
If ASYNCP is t, `appwrite--post-api' will be asynchronous.
|
|
|
|
|
CALLBACK must then be set as it will be called once the request
|
2022-07-13 13:49:50 +00:00
|
|
|
|
finishes. See `url-retrieve'.
|
2022-07-13 13:34:50 +00:00
|
|
|
|
|
2022-07-13 16:52:58 +00:00
|
|
|
|
EXTRA-HEADERS is a list of pairs to append to
|
|
|
|
|
`url-request-extra-headers'.
|
|
|
|
|
|
2022-07-13 13:34:50 +00:00
|
|
|
|
The function returns a pair composed of the HTTP status code as
|
2022-07-13 13:49:50 +00:00
|
|
|
|
its car. The cdr is a hash table from the response answer if
|
2022-07-13 13:34:50 +00:00
|
|
|
|
Content-Type in the headers is \"application/json\"."
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(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)))
|
2022-07-13 16:52:58 +00:00
|
|
|
|
(url-request-extra-headers (if extra-headers
|
|
|
|
|
(append url-request-extra-headers
|
|
|
|
|
extra-headers)
|
|
|
|
|
url-request-extra-headers))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(url-request-data (cond ((not (string= content-type "application/json"))
|
2022-07-13 13:34:50 +00:00
|
|
|
|
payload)
|
2022-07-13 13:43:47 +00:00
|
|
|
|
((appwrite--plistp payload)
|
|
|
|
|
(json-encode-plist payload))
|
2022-07-13 13:34:50 +00:00
|
|
|
|
((hash-table-p payload) (json-encode payload))
|
|
|
|
|
(payload-alist-p (json-encode-alist payload))
|
|
|
|
|
(t payload))))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(if asyncp
|
|
|
|
|
(url-retrieve url callback)
|
|
|
|
|
(with-current-buffer (url-retrieve-synchronously url)
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(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))))))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Account
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Users
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Teams
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Databases
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Storage
|
|
|
|
|
|
2022-07-13 14:35:28 +00:00
|
|
|
|
(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.
|
2022-07-13 00:01:17 +00:00
|
|
|
|
|
|
|
|
|
PERMISSION is the permissioin type model to use for reading files
|
2022-07-13 13:49:50 +00:00
|
|
|
|
in this bucket. By default, PERMISSION is \"bucket\". For more
|
2022-07-13 00:01:17 +00:00
|
|
|
|
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."
|
2022-07-13 14:35:28 +00:00
|
|
|
|
(let ((payload `(bucketId ,id name ,name permission ,permission))
|
|
|
|
|
(method (if updatep "PUT" "POST")))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(when read
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(setq payload (append payload `(read ,read))))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(when write
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(setq payload (append payload `(write ,write))))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(when maximum-file-size
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(setq payload (append payload `(maximumFileSize ,maximum-file-size))))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
(when allowed-file-extensions
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(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)
|
2022-07-13 14:35:28 +00:00
|
|
|
|
(let ((response (appwrite--query-api :method method
|
|
|
|
|
:api (concat "/v1/storage/buckets/"
|
|
|
|
|
(if updatep id ""))
|
2022-07-13 13:34:50 +00:00
|
|
|
|
:payload (json-encode-plist payload))))
|
2022-07-13 14:35:28 +00:00
|
|
|
|
(appwrite--process-response (format "Failed to %s bucket %s"
|
|
|
|
|
(if updatep "update" "create")
|
|
|
|
|
id)
|
|
|
|
|
(if updatep 200 201)
|
2022-07-13 13:34:50 +00:00
|
|
|
|
response))))
|
|
|
|
|
|
2022-07-13 14:35:28 +00:00
|
|
|
|
(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))
|
|
|
|
|
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(cl-defun appwrite-storage-list-buckets (&key search (limit 25) offset cursor cursor-direction order-type)
|
|
|
|
|
"List of all storage buckets.
|
|
|
|
|
|
2022-07-13 13:49:50 +00:00
|
|
|
|
SEARCH is a string to filter the list results when non-nil. Max
|
2022-07-13 13:34:50 +00:00
|
|
|
|
length of 256 chars.
|
|
|
|
|
|
|
|
|
|
LIMIT is the maximum amount of buckets returned by the
|
|
|
|
|
query (default value: 25).
|
|
|
|
|
|
|
|
|
|
OFFSET is the results offset with which the user can manage the
|
|
|
|
|
pagination of the results when non-nil.
|
|
|
|
|
|
|
|
|
|
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
|
2022-07-13 13:49:50 +00:00
|
|
|
|
acquired JSON. Otherwise, return nil and warn the user."
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(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 cursor-direction (setq payload (append payload `(cursor-direction ,cursor-direction))))
|
|
|
|
|
(when order-type (setq payload (append payload `(order-type ,order-type))))
|
|
|
|
|
(let* ((response (appwrite--query-api :api "/v1/storage/buckets"
|
|
|
|
|
:payload (json-encode-plist payload)))
|
|
|
|
|
(status (car response))
|
|
|
|
|
(json (cdr response)))
|
|
|
|
|
(if (eq 200 status)
|
|
|
|
|
json
|
|
|
|
|
(appwrite--message-failure "Failed to list buckets" 200 (gethash "message" json))))))
|
|
|
|
|
|
|
|
|
|
(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)))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
|
|
|
|
|
(defun appwrite-storage-delete-bucket (id)
|
|
|
|
|
"Delete bucket with id ID."
|
2022-07-13 13:34:50 +00:00
|
|
|
|
(let ((response (appwrite--query-api :method "DELETE"
|
|
|
|
|
:api (concat "/v1/storage/buckets/" id))))
|
|
|
|
|
(appwrite--process-response (concat "Failed to delete bucket " id)
|
|
|
|
|
204
|
|
|
|
|
response)))
|
2022-07-13 00:01:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Functions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Localization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Avatars
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Health
|
|
|
|
|
|
|
|
|
|
(provide 'appwrite)
|
|
|
|
|
;;; appwrite.el ends here
|