summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-01 19:32:52 -0600
committerChristian Cleberg <[email protected]>2026-03-01 19:32:52 -0600
commita98c2a4542e19ce4fb632bc32fc47c18273161cc (patch)
tree99d016c1c9f12d98d57cfec34262e7c228a1d920 /main.go
parentcd8c38db601355003510e92a0936cde6b71cf849 (diff)
downloadmichelangelo-a98c2a4542e19ce4fb632bc32fc47c18273161cc.tar.gz
michelangelo-a98c2a4542e19ce4fb632bc32fc47c18273161cc.tar.bz2
michelangelo-a98c2a4542e19ce4fb632bc32fc47c18273161cc.zip
convert from PHP to Go
Diffstat (limited to 'main.go')
-rw-r--r--main.go266
1 files changed, 266 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..c74ee5e
--- /dev/null
+++ b/main.go
@@ -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}`)
+}