git.fiddlerwoaroof.com
inangulis.lisp
c27cda0b
 (declaim (optimize (debug 3) (speed 0 ) (safety 3)) )
4d21fd6e
 
6ba527a5
 ;;;; inangulis.lisp
c27cda0b
 
6ba527a5
 (in-package #:inangulis)
c27cda0b
 (defparameter *submissions* nil)
 (defparameter *by-distinct* (make-hash-table :test #'equalp))
 (defparameter *users* (make-hash-table :test #'equalp))
4d21fd6e
 (defparameter *persist* t)
 
 (defmacro cdr-assoc (&whole lis item alist &key key test test-not)
   `(cdr (assoc ,@(cdr lis))))
 
 (defun str-assoc (param params &key (test #'equal) key)
   (cdr-assoc param params :test test :key key))
 
6ba527a5
 ;;; "inangulis" goes here. Hacks and glory await!
c27cda0b
 
 (defun current-date-string ()
   "Returns current date as a string."
4d21fd6e
   (local-time:format-timestring nil (local-time:now) 
                                 :format local-time:+rfc-1123-format+))
c27cda0b
 
 (defclass user ()
   ((uid :initarg :uid)
    (email :initarg :email)
    (name :initarg :name)
    (moderator :initarg :moderator :initform nil)))
 
 (defun user-alist (user)
b55a8fe2
   (with-slots ((email inangulis.tables::email)
                (name inangulis.tables::name)
                (moderator inangulis.tables::moderator)) user
c27cda0b
     `(("name" . ,name)
       ("email" . ,email)
       ("moderator" . ,moderator))))
 
 (defmethod print-object ((obj user) s)
   (print-unreadable-object (obj s :type t :identity t)
     (with-slots (uid email name moderator) obj
       (format s "U: ~s E: ~s N: ~s M: ~s" uid email name moderator))))
 
 (defun make-submission (headline url &key (approved ""))
4d21fd6e
   (alet 'inangulis.tables:submission
     (make-instance it :headline headline :url url :approved approved :date (current-date-string))))
c27cda0b
 
 (defun submission-alist (submission)
   `(("headline". ,(s-headline submission))
     ("url" . ,(s-url submission))
     ("date" . ,(s-url submission))
     ("approved" . ,(s-approved submission))))
 
4d21fd6e
 (defun get-by-key (headline url)
   (car (postmodern:select-dao 'inangulis.tables::submission (:and (:= 'headline headline) (:= 'url url)))))
 
 
 (defun alist-submission (alist &key nil-if-exists)
   (let* ((headline (cdr-assoc :headline alist :test #'string-equal))
          (url (cdr-assoc :url alist :test #'string-equal))
          (approved (or (cdr-assoc :approved alist :test #'string-equal) ""))   
          (result (make-submission headline url :approved approved)))
     (with-slots ((headline inangulis.tables:headline) (url inangulis.tables:url)) result
       (aif (get-by-key headline url)
         (unless nil-if-exists it)
c27cda0b
         result))))
 
 
 (defun get-moderated (feeds)
   (remove-if (lambda (x) (not (equal x "approved"))) feeds :key #'s-approved))
 
 (defparameter *app* (make-instance 'ningle:<app>))
8aeb7f31
 
 (defun rebind-headers (app)
   (lambda (env)
     (format t "~&~%------------------@!@#$@#!$!@#$-----------------~%")
     (format t "~&~%~s"   ningle.context:*request*)
     (format t "~&~s~%" (alexandria:hash-table-alist (lack.request:request-headers ningle.context:*request*)))
     (format t "~&~s~%~%" (gethash "x-real-ip" (lack.request:request-headers ningle.context:*request*)))
     (awhen (gethash "x-real-ip" (lack.request:request-headers ningle.context:*request*))
       (format t "~&~s"   (lack.request:request-remote-addr ningle.context:*request*))  
       (setf (lack.request:request-remote-addr ningle:*request*) it)
       (setf (getf env :remote-addr) it)
       (format t "~&~s"   (lack.request:request-remote-addr ningle.context:*request*)))
     (funcall app env)))
c27cda0b
 
4d21fd6e
 (defmacro i-defun (name (&rest args) &body body)
   `(defun ,name (,@args)
      (declare (ignorable ,@(loop for x in args
                                  if (not (char= (elt (symbol-name x) 0) #\&))
                                  collect x)))
      ,@body))
 
c27cda0b
 (defmacro i-lambda ((&rest args) &body body)
   `(lambda (,@args)
      (declare (ignorable ,@args))
      ,@body))
 
4d21fd6e
 (defmacro with-db (&body b)
   `(postmodern:with-connection (ubiquitous:value 'db)
      ,@b))
 
 (defmacro with-submissions (&body b)
   `(with-db
      (let ((*submissions* (postmodern:select-dao 'inangulis.tables::submission t "date desc")))
        ,@b)))
 
 
b55a8fe2
 
c27cda0b
 
 (defun submit (params)
b55a8fe2
   (awhen (alist-submission params :nil-if-exists t)
     (postmodern:insert-dao it)
     (push it *submissions*)))
c27cda0b
 
 (defun get-feed-guid (item)
   (with-slots (alimenta:title alimenta:link) item
     (let ((hasher (ironclad:make-digest 'ironclad:sha256)))
4b08ceb7
       (ironclad:update-digest hasher (babel:string-to-octets alimenta:title))
       (ironclad:update-digest hasher (babel:string-to-octets alimenta:link))
c27cda0b
       (ironclad:byte-array-to-hex-string (ironclad:produce-digest hasher)))))
 
b55a8fe2
 (defmethod run-route :around (name params &rest r)
   (declare (ignore r))
   (with-db
     (pomo:with-transaction ()
       (call-next-method))))
 
4d21fd6e
 ;; View Controllers
b55a8fe2
 (define-controller murmur (params)
4d21fd6e
   (sleep 0.01)
b55a8fe2
   (submit params))
 
 (define-view murmur (model)
   '(302 (:location "/") ("Done"))) 
   
 (define-controller curate (params)
   (let ((*submissions* (postmodern:select-dao 'inangulis.tables::submission t "date desc")))
     (cl-oid-connect.utils:require-login
       (alet (alist-submission params)
         (let ((approval (string-downcase (str-assoc "approved" params :test #'equalp))))
           (setf (s-approved it)
                 (if (equal approval "+") "approved"
                   (if (equal approval "-") "rejected")))
           (when *persist*
             (postmodern:update-dao it)))))))
 
 (define-view curate (params)
4d21fd6e
   '(302 (:location "/") ("Done")))
 
b55a8fe2
 (define-view login-page (model)
   `(200
     ()
4d21fd6e
     (,(cl-who:with-html-output-to-string (s)
         (:html
           (:head
             (:title "Login")
             (:link :rel "stylesheet" :href "/static/css/login.css"))
           (:body
             (:h1 "In Angulis")
             (:div :class "login-buttons"
              (:a :class "facebook" :href "/login/facebook" "Login With Facebook"))))))))
 
b55a8fe2
 (define-controller logout (params)
4d21fd6e
   (ningle:with-context-variables (session)
b55a8fe2
     (setf (gethash :userinfo session) nil)))
4d21fd6e
 
b55a8fe2
 (define-view logout (model)
   '(302 (:location "/")))
 
 (defmethod controller ((name (eql 'get-feed)) params &key moderated)
4d21fd6e
   (let ((feed (alimenta::make-feed :title "In Angulis" :link "http://in-angulis.com/feed"
                                    :description "Locus in quo sunt illi qui murmurant in angulis")))
b55a8fe2
     (prog1 feed
4d21fd6e
       (pomo:do-select-dao (('inangulis.tables::submission submission)
                            (:raw (if moderated (pomo:sql (:= 'approved "approved")) "'t'"))
                            (:desc 'date))
         (alimenta::add-item-to-feed feed
                                     :title (s-headline submission)
                                     :link (s-url submission)
                                     :date (s-date submission)
                                     :next-id #'get-feed-guid
b55a8fe2
                                     :content "")))))
 
 (defparameter *tmp* nil)
 
 (define-view get-feed (feed)
   `(200 (:content-type "application/rss+xml") (,(plump:serialize (alimenta:generate-xml feed) nil))))
 
 (mustache-view root (user . links) #p"static/index.mustache.html"
   :links (mapcar #'submission-alist links)
   :user (when user (user-alist user)))
 
 (define-controller root (params)
   (setf *tmp* ningle.context:*request*)
   (with-submissions
     (ningle:with-context-variables (session)
       (handler-case
         (cl-oid-connect.utils:ensure-logged-in
           (cl-oid-connect.utils:redirect-if-necessary session
             (cons (gethash :app-user session)
                   *submissions*)))
         (cl-oid-connect.utils:user-not-logged-in (c) (cons nil *submissions*))))))
 
 (defmethod controller :around ((name (eql 'headlines)) params &key (moderated t moderated-p))
   (unless moderated-p
     (awhen (str-assoc "moderated" params :test #'string-equal)
            (setf moderated (and (> (length it) 0) (char= #\t (elt it 0)))
                  moderated-p t)))
   (if moderated-p
     (call-next-method name params :moderated moderated)  
     (call-next-method)))
 
 (defmethod controller ((name (eql 'headlines)) params &key (moderated t moderated-p))
   (let (result)
     (ningle.context:with-context-variables (session)
       (let* ((app-user (gethash :app-user session))
              (moderator-p (and app-user (slot-value app-user 'inangulis.tables::moderator))))
         (pomo:do-select-dao (('inangulis.tables::submission submission)
                              (:raw (cond 
                                      ((and moderator-p (not moderated-p)) "'t'")
                                      (moderated (pomo:sql (:= 'approved "approved")))
                                      (t "'t'")))
                              (:desc 'date))
           (push submission result))))
     result))
 
 (define-view headlines (columns) ;#p"static/headlines.mustache.html"
   (let ((sub-len (length columns))
         (columns (mapcar #'submission-alist columns)))
     (render-mustache #p"static/headlines.mustache.html"
                      `((:columns . (((:rows .  ,(subseq columns 0 (floor sub-len 3))))
                                     ((:rows .  ,(subseq columns (floor sub-len 3) (* 2 (floor sub-len 3)) )))
                                     ((:rows .  ,(subseq columns (* 2 (floor sub-len 3)) sub-len)))))))))
 
 (defroutes *app*
   (("/") (as-route 'root))
   (("/curo" :method :POST) (as-route 'curate)) 
   (("/feed") (as-route 'get-feed :moderated t))
   (("/firehose") (as-route 'get-feed))
   (("/headlines") (as-route 'headlines)) 
   (("/login") (as-route 'login-page))
   (("/logout") (as-route 'logout)) 
   (("/murmuro" :method :POST) (as-route 'murmur)))
c27cda0b
 
 (cl-oid-connect::setup-oid-connect *app* (userinfo &rest args)
   (declare (ignore args))
   (let ((id (cdr (assoc :id userinfo))))
b55a8fe2
     (with-db
       (aif (car (postmodern:select-dao 'inangulis.tables::user (:= 'uid id)))
         it
         (alet (make-instance 'inangulis.tables::user)
           (with-slots ((uid inangulis.tables::uid)
                        (name inangulis.tables::name)
                        (email inangulis.tables::email)) it
             (prog1 it
               (setf uid id
                     name (cdr (assoc :name userinfo))
                     email (cdr (assoc :email userinfo)))
               (pomo:with-transaction ()
                 (postmodern:insert-dao it)))))))))
 
 
 
c27cda0b
 
 (let ((handler nil))
5519ae07
   (ubiquitous:restore :inangulis)
c27cda0b
   (defun stop () (clack:stop (pop handler)))
 
   (defun start (&optional tmp)
     (cl-oid-connect:initialize-oid-connect
       (ubiquitous:value 'facebook 'secrets)
       (ubiquitous:value 'google 'secrets))
     (let ((server (if (> (length tmp) 1)
                     (intern (string-upcase (elt tmp 1)) 'keyword)
                     :hunchentoot)))
       (push (clack:clackup
ea862013
               (funcall clack-errors:*clack-error-middleware*
                        (lack.builder:builder
4b08ceb7
                          :accesslog
ea862013
                          (:static :path "/static/" :root #p"./static/")
                          :backtrace
8aeb7f31
                          :session
ea862013
                          *app*
f6790fb4
                          )
b55a8fe2
                       :debug (ubiquitous:defaulted-value t 'debug))
c27cda0b
               :port 9090
               :server server)
             handler)))
 
   (defun restart-clack ()
     (do () ((null handler)) (stop))
     (start)))