;;; appwrite.el --- Appwrite server SDK -*- lexical-binding: t; -*- ;; Copyright (C) 2022 Lucien Cartier-Tilet ;; Author: Lucien Cartier-Tilet ;; Maintainer: Lucien Cartier-Tilet ;; 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 . ;;; 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 it’s 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 that’s 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