diff --git a/asteroid.asd b/asteroid.asd
index 7cae886..117038b 100644
--- a/asteroid.asd
+++ b/asteroid.asd
@@ -36,6 +36,7 @@
:r-data-model
(:interface :auth)
(:interface :database)
+ (:interface :rate)
(:interface :user))
:pathname "./"
:components ((:file "app-utils")
@@ -43,6 +44,7 @@
(:module :config
:components ((:file radiance-postgres)))
(:file "conditions")
+ (:file "limiter")
(:file "database")
(:file "template-utils")
(:file "parenscript-utils")
diff --git a/limiter.lisp b/limiter.lisp
new file mode 100644
index 0000000..84f79e1
--- /dev/null
+++ b/limiter.lisp
@@ -0,0 +1,56 @@
+;;;; limiter.lisp - Rate limiter definitions for the application
+
+(in-package :asteroid)
+
+(defun render-rate-limit-error-page()
+ (clip:process-to-string
+ (load-template "error")
+ :error-message "It seems that your acceleration has elevated your orbit out of your designated path."
+ :error-action "Please wait a moment for it to stabilize and try your request again."))
+
+(defun api-limit-error-output ()
+ (api-output `(("status" . "error")
+ ("message" . "It seems that your acceleration has elevated your orbit out of your designated path."))
+ :message "It seems that your acceleration has elevated your orbit out of your designated path."
+ :status 429))
+
+(defun extract-limit-options (options)
+ "Extracts the rate-limit options and forwards the reamaining radiance route options"
+ (let ((limit (getf options :limit))
+ (timeout (getf options :timeout))
+ (group (getf options :limit-group))
+ (rest (loop for (k v) on options by #'cddr
+ unless (member k '(:limit :timeout :limit-group))
+ append (list k v))))
+ (values limit timeout group rest)))
+
+
+(defmacro define-page-with-limit (name uri options &body body)
+ "Rate limit for a page route. Defaults to 30 requests per minute."
+ (multiple-value-bind (limit timeout group rest) (extract-limit-options options)
+ (let* ((limit-name (string-upcase (format nil "~a-route-limit" (or group name))))
+ (limit-sym (intern limit-name))
+ (limit (or limit 30))
+ (timeout (or timeout 60)))
+ `(eval-when (:compile-toplevel :load-toplevel :execute)
+ (rate:define-limit ,limit-sym (time-left :limit ,limit :timeout ,timeout)
+ ;; (format t "Route limit '~a' hit. Wait ~a seconds and retry.~%" ,(string name) time-left)
+ (render-rate-limit-error-page))
+ (define-page ,name ,uri ,rest
+ (rate:with-limitation (,limit-sym)
+ ,@body))))))
+
+(defmacro define-api-with-limit (name args options &body body)
+ "Rate limit for api routes. Defaults to 60 requests per minute."
+ (multiple-value-bind (limit timeout group rest) (extract-limit-options options)
+ (let* ((limit-name (string-upcase (format nil "~a-api-limit" (or group name))))
+ (limit-sym (intern limit-name))
+ (limit (or limit 60))
+ (timeout (or timeout 60)))
+ `(eval-when (:compile-toplevel :load-toplevel :execute)
+ (rate:define-limit ,limit-sym (time-left :limit ,limit :timeout ,timeout)
+ ;; (format t "API Rate limit '~a' hit. Wait ~a seconds and retry.~%" ,(string name) time-left)
+ (api-limit-error-output))
+ (define-api ,name ,args ,rest
+ (rate:with-limitation (,limit-sym)
+ ,@body))))))
diff --git a/template/error.ctml b/template/error.ctml
new file mode 100644
index 0000000..3907fc4
--- /dev/null
+++ b/template/error.ctml
@@ -0,0 +1,57 @@
+
+
+
+ Error - Asteroid Radio
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⚠️ Something went wrong with your request!
+
+
+
+
+
+
+
+
+
+ We seem to be unable to process your request right now.
+
+
+
+
+
+
+
+
+
+
+
+
+