diff options
Diffstat (limited to 'main.go')
| -rw-r--r-- | main.go | 266 |
1 files changed, 266 insertions, 0 deletions
@@ -0,0 +1,266 @@ +package main + +import ( + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "os" + "strconv" + "strings" +) + +var tmpl = template.Must(template.ParseFiles("templates/layout.html")) + +func main() { + http.HandleFunc("/", handleGallery) + http.HandleFunc("/blog/", handleBlog) + http.HandleFunc("/search", handleSearch) + http.HandleFunc("/auth/callback", handleAuthCallback) + http.HandleFunc("/logout", handleLogout) + + // JSON API endpoints (called by frontend JS) + http.HandleFunc("/api/dashboard", handleAPIDashboard) + http.HandleFunc("/api/blog/", handleAPIBlog) + http.HandleFunc("/api/search", handleAPISearch) + http.HandleFunc("/api/like", handleAPILike) + http.HandleFunc("/api/reblog", handleAPIReblog) + + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + log.Printf("Michelangelo running on :%s", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} + +// --- Auth helpers --- + +func getTokens(r *http.Request) (token, secret string) { + c1, err1 := r.Cookie("perm_token") + c2, err2 := r.Cookie("perm_secret") + if err1 != nil || err2 != nil { + return "", "" + } + return c1.Value, c2.Value +} + +func requireAuth(w http.ResponseWriter, r *http.Request) (string, string, bool) { + token, secret := getTokens(r) + if token == "" || secret == "" { + startOAuth(w, r) + return "", "", false + } + return token, secret, true +} + +func startOAuth(w http.ResponseWriter, r *http.Request) { + consumerKey := os.Getenv("CONSUMER_KEY") + consumerSecret := os.Getenv("CONSUMER_SECRET") + tmpToken, tmpSecret, err := GetRequestToken(consumerKey, consumerSecret) + if err != nil { + http.Error(w, "OAuth init failed: "+err.Error(), 500) + return + } + http.SetCookie(w, &http.Cookie{Name: "tmp_token", Value: tmpToken, Path: "/", HttpOnly: true}) + http.SetCookie(w, &http.Cookie{Name: "tmp_secret", Value: tmpSecret, Path: "/", HttpOnly: true}) + http.Redirect(w, r, "https://www.tumblr.com/oauth/authorize?oauth_token="+tmpToken, http.StatusFound) +} + +func handleAuthCallback(w http.ResponseWriter, r *http.Request) { + verifier := r.URL.Query().Get("oauth_verifier") + if verifier == "" { + http.Error(w, "Missing oauth_verifier", 400) + return + } + tmpToken, err1 := r.Cookie("tmp_token") + tmpSecret, err2 := r.Cookie("tmp_secret") + if err1 != nil || err2 != nil { + http.Error(w, "Missing temporary tokens", 400) + return + } + consumerKey := os.Getenv("CONSUMER_KEY") + consumerSecret := os.Getenv("CONSUMER_SECRET") + permToken, permSecret, err := GetAccessToken(consumerKey, consumerSecret, tmpToken.Value, tmpSecret.Value, verifier) + if err != nil { + http.Error(w, "OAuth exchange failed: "+err.Error(), 500) + return + } + http.SetCookie(w, &http.Cookie{Name: "perm_token", Value: permToken, Path: "/", HttpOnly: true}) + http.SetCookie(w, &http.Cookie{Name: "perm_secret", Value: permSecret, Path: "/", HttpOnly: true}) + http.Redirect(w, r, "/", http.StatusFound) +} + +func handleLogout(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{Name: "perm_token", Value: "", MaxAge: -1, Path: "/"}) + http.SetCookie(w, &http.Cookie{Name: "perm_secret", Value: "", MaxAge: -1, Path: "/"}) + http.Redirect(w, r, "/", http.StatusFound) +} + +// --- Page handlers (render shell, JS does the loading) --- + +type PageData struct { + Title string + BlogName string + QueryOrBlog string + View string // "dashboard" | "blog" | "search" +} + +func handleGallery(w http.ResponseWriter, r *http.Request) { + token, secret, ok := requireAuth(w, r) + if !ok { + return + } + client := NewClient(os.Getenv("CONSUMER_KEY"), os.Getenv("CONSUMER_SECRET"), token, secret) + info, err := client.GetUserInfo() + blogName := "" + if err == nil && len(info.Response.User.Blogs) > 0 { + blogName = info.Response.User.Blogs[0].Name + } + tmpl.Execute(w, PageData{Title: "Michelangelo", BlogName: blogName, View: "dashboard"}) +} + +func handleBlog(w http.ResponseWriter, r *http.Request) { + _, _, ok := requireAuth(w, r) + if !ok { + return + } + name := strings.TrimPrefix(r.URL.Path, "/blog/") + name = strings.Trim(name, "/") + if name == "" { + http.Redirect(w, r, "/", http.StatusFound) + return + } + tmpl.Execute(w, PageData{Title: name + " — Michelangelo", QueryOrBlog: name, View: "blog"}) +} + +func handleSearch(w http.ResponseWriter, r *http.Request) { + _, _, ok := requireAuth(w, r) + if !ok { + return + } + q := r.URL.Query().Get("q") + tmpl.Execute(w, PageData{Title: "Search — Michelangelo", QueryOrBlog: q, View: "search"}) +} + +// --- API handlers (JSON, consumed by frontend) --- + +func jsonError(w http.ResponseWriter, msg string, code int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + fmt.Fprintf(w, `{"error":%q}`, msg) +} + +func getClient(w http.ResponseWriter, r *http.Request) (*TumblrClient, bool) { + token, secret, ok := requireAuth(w, r) + if !ok { + return nil, false + } + return NewClient(os.Getenv("CONSUMER_KEY"), os.Getenv("CONSUMER_SECRET"), token, secret), true +} + +func handleAPIDashboard(w http.ResponseWriter, r *http.Request) { + client, ok := getClient(w, r) + if !ok { + return + } + offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) + postType := r.URL.Query().Get("type") + if postType == "" { + postType = "photo" + } + posts, err := client.GetDashboard(offset, 20, postType) + if err != nil { + jsonError(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(posts) +} + +func handleAPIBlog(w http.ResponseWriter, r *http.Request) { + client, ok := getClient(w, r) + if !ok { + return + } + name := strings.TrimPrefix(r.URL.Path, "/api/blog/") + name = strings.Trim(name, "/") + offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) + postType := r.URL.Query().Get("type") + if postType == "" { + postType = "photo" + } + posts, err := client.GetBlogPosts(name, offset, 20, postType) + if err != nil { + jsonError(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(posts) +} + +func handleAPISearch(w http.ResponseWriter, r *http.Request) { + client, ok := getClient(w, r) + if !ok { + return + } + q := r.URL.Query().Get("q") + if q == "" { + jsonError(w, "missing query", 400) + return + } + posts, err := client.GetTagged(q) + if err != nil { + jsonError(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(posts) +} + +func handleAPILike(w http.ResponseWriter, r *http.Request) { + client, ok := getClient(w, r) + if !ok { + return + } + id := r.URL.Query().Get("id") + key := r.URL.Query().Get("key") + unlike := r.URL.Query().Get("unlike") == "1" + var err error + if unlike { + err = client.UnlikePost(id, key) + } else { + err = client.LikePost(id, key) + } + if err != nil { + jsonError(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"ok":true}`) +} + +func handleAPIReblog(w http.ResponseWriter, r *http.Request) { + client, ok := getClient(w, r) + if !ok { + return + } + if err := r.ParseForm(); err != nil { + jsonError(w, "bad request", 400) + return + } + blogName := r.FormValue("blog_name") + id := r.FormValue("id") + key := r.FormValue("reblog_key") + nativeBlog := r.FormValue("native_blog") + err := client.ReblogPost(nativeBlog, id, key, blogName) + if err != nil { + jsonError(w, err.Error(), 500) + return + } + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{"ok":true}`) +} |
