dude-remember / src / duderemember.clj

(ns duderemember
  (:gen-class :extends javax.servlet.http.HttpServlet)
  (:use appengine.datastore
        appengine.users 
        clojure.contrib.prxml
        compojure.core
        hiccup.core
        hiccup.form-helpers
        hiccup.page-helpers
        ring.util.response
        ring.util.servlet)
  (:require [compojure.route :as route]
            [clojure.contrib.logging :as log]))

(defn digits-only [phone]
  (apply str (re-seq #"[0-9]+" phone)))

(defentity User ()
  ((user-id) ; We call it user-id to match Google user object
   (phone)))

(defentity Message ()
  ((user-id)
   (time)
   (text)
   (url)))

(defn phone->user [phone]
  (let [users (find-users-by-phone phone)]
    (if (zero? (count users))
      nil
      (first users))))

(defn user->phone [user]
  (let [entities (find-users-by-user-id (:user-id user))]
    (if (zero? (count entities))
      nil
      (:phone (first entities)))))

(defn new-user? [user]
  (nil? (user->phone user)))

(defn update-user [u phone]
  (let [phone (digits-only phone)]
    (if (new-user? u)
      (save-entity (user {:user-id (:user-id u) :phone phone}))
      (let [entity (first (find-users-by-user-id (:user-id u)))]
        (update-entity entity {:phone phone})
        (save-entity entity)))))

(defn now []
  (System/currentTimeMillis))

(defn add-sms [user text]
  (let [entity (message {:user-id (:user-id user) :text text :time (now)})]
    (save-entity entity)))

(defn add-voice [user url]
  (let [entity (message {:user-id (:user-id user) :url url :time (now)})]
    (save-entity entity)))

(defn user-messages [user]
  (select "message"
    where (= :user-id (:user-id user))
    order-by (:time :desc)))

(def *title* "Dude! ... Remember!")

(defn header-link []
  (if (nil? (current-user))
    [:a {:href (login-url "/")} "Login"]
    [:a {:href (logout-url "/")} "Logout"]))

(defn page [& content]
  (html
    [:head
     (include-css "/static/dude.css")
     [:title *title*]]
    [:body 
     [:div {:class "header"} (header-link)]
     [:h1 [:a {:href "/"} [:img {:src "/static/sms.png"}]] *title*]
     content]))

(defn format-time [t]
  (let [date (new java.util.Date)]
    (.setTime date t)
    (str date)))

(defn render-messages [messages]
  (if (zero? (count messages))
    "No messages for you!"
    (for [message messages]
      [:div {:class "message" :id (key->string (:key message))}
       "Time: " (format-time (:time message)) [:br]
       (if (nil? (:url message))
         ["Text:" [:br] (:text message)]
         [:a {:href (:url message)} "Play"])])))

(defn format-phone [phone]
  (format "%s-%s-%s" (subs phone 0 3) (subs phone 3 6) (subs phone 6)))

(defn user-page [user]
  (let [messages (user-messages user)
        phone (format-phone (user->phone user))]
    (page
      [:h2 "Messages for " (:nickname user) " (" 
           [:a {:href "/phone" } phone] ")"]
      (render-messages messages))))

(defn eval-page [expr]
  (page
    (str (load-string (str "(in-ns 'duderemember)" expr)))
    (form-to [:post]
      [:br]
      (text-field :expr)
      (submit-button "Eval"))))

(defn phone-page []
  (page
    "Please update your phone number" [:br]
    (form-to [:post]
       (text-field :phone)
       (submit-button "Update"))))

(defn twilio-response [& params]
  (with-out-str (prxml [:Response params])))

; Remove the +1 suffix
(defn normalize-phone [phone]
  (subs phone 2))

(defn twilio-sms [params]
  (log/info (str "SMS: " params))
  (let [phone (normalize-phone (params "From"))
        text (params "Body")
        user (phone->user phone)]
    (if user
      (do (add-sms user text)
        (twilio-response))
      ; Can't find user, SMS back how to register
      (twilio-response [:Sms "Dude, I don't know you! "
                            "Check out http://dude-remember.appspot.com"]))))

(defn twilio-voice [params]
  (log/info (str "VOICE: " params))
  (let [phone (normalize-phone (params "From"))
        user (phone->user phone)]
    (if (nil? user)
      (twilio-response [:Say {:voice "woman"} "Dude, I don't know you. Ciao!"]
                      [:Hangup])
      (let [url (params "RecordingUrl")]
        (if (nil? url) ; New call
          (twilio-response [:Say {:voice "woman"} "Talk to me babe!"]
                          [:Record {:maxLength "30"}])
          (do (add-voice user url)
            (twilio-response [:Say {:voice "woman"} "Gotcha! Later dude."]
                            [:Hangup])))))))
(defn general-index []
  (page
    [:div "This is a simple service where you send notifications to yourself
          via voice calls or SMS"]
    [:div "All you need to do is to " [:a {"href" (login-url "/")} "login"]
          " using your Google account and update your phone."]))

(defn can-delete [user message]
  (let [user-id (:user-id user)]
    (and (not (nil? user-id)) (= user-id (:user-id message)))))

(defn delete-message [id]
  (let [message-key (string->key id)
        messages (select "message" where (= :key message-key))]
    (if (empty? messages)
      (do (log/error (format "DELETE %s - not found" id))
          {:status 404 :body (format "Eeek, can't find %s!" id)})
      (let [message (first messages)]
        (if (not (can-delete (current-user) message))
          (do 
            (log/error (format "Unauthorized: %s delete %s" (current-user) id))
            {:status 401 :body "No DELETE for you!"})
          (do (delete-entity message-key)
              {:status 200 :body "OK"}))))))
             
(defn welcome-page []
  (let [user (current-user)]
    (if (nil? user)
      (general-index)
      (if (new-user? user)
        (redirect "/phone")
        (user-page user)))))

(defroutes duderemeber-routes
  (GET "/" [] (welcome-page))

  (GET "/phone" [] (phone-page))
  (POST "/phone" {params :params}
        (let [phone (params "phone")]
          (update-user (current-user) phone)
          (redirect "/")))
  (DELETE "/message/:id" [id] (delete-message id))

  (POST "/twilio/sms" {params :params} (twilio-sms params))
  (POST "/twilio/voice" {params :params} (twilio-voice params))

; Uncomment these when debugging, *don't* let them slip to production           
;  (GET "/_eval" [] (eval-page "0"))
;  (POST "/_eval" {params :params} (eval-page (params "expr")))

  (route/not-found "Dude! I can't find it."))

(defservice duderemeber-routes)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.