krz/michelangelo

clone: git clone https://gitbay.org/krz/michelangelo.git

main: main.go · raw

  1package main
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"html/template"
  7	"log"
  8	"net/http"
  9	"os"
 10	"strconv"
 11	"strings"
 12)
 13
 14var tmpl = template.Must(template.ParseFiles("templates/layout.html"))
 15
 16func main() {
 17	http.HandleFunc("/", handleGallery)
 18	http.HandleFunc("/blog/", handleBlog)
 19	http.HandleFunc("/search", handleSearch)
 20	http.HandleFunc("/auth/callback", handleAuthCallback)
 21	http.HandleFunc("/logout", handleLogout)
 22
 23	// JSON API endpoints (called by frontend JS)
 24	http.HandleFunc("/api/dashboard", handleAPIDashboard)
 25	http.HandleFunc("/api/blog/", handleAPIBlog)
 26	http.HandleFunc("/api/search", handleAPISearch)
 27	http.HandleFunc("/api/like", handleAPILike)
 28	http.HandleFunc("/api/reblog", handleAPIReblog)
 29
 30	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
 31
 32	port := os.Getenv("PORT")
 33	if port == "" {
 34		port = "8080"
 35	}
 36	log.Printf("Michelangelo running on :%s", port)
 37	log.Fatal(http.ListenAndServe(":"+port, nil))
 38}
 39
 40// --- Auth helpers ---
 41
 42func getTokens(r *http.Request) (token, secret string) {
 43	c1, err1 := r.Cookie("perm_token")
 44	c2, err2 := r.Cookie("perm_secret")
 45	if err1 != nil || err2 != nil {
 46		return "", ""
 47	}
 48	return c1.Value, c2.Value
 49}
 50
 51func requireAuth(w http.ResponseWriter, r *http.Request) (string, string, bool) {
 52	token, secret := getTokens(r)
 53	if token == "" || secret == "" {
 54		startOAuth(w, r)
 55		return "", "", false
 56	}
 57	return token, secret, true
 58}
 59
 60func startOAuth(w http.ResponseWriter, r *http.Request) {
 61	consumerKey := os.Getenv("CONSUMER_KEY")
 62	consumerSecret := os.Getenv("CONSUMER_SECRET")
 63	tmpToken, tmpSecret, err := GetRequestToken(consumerKey, consumerSecret)
 64	if err != nil {
 65		http.Error(w, "OAuth init failed: "+err.Error(), 500)
 66		return
 67	}
 68	http.SetCookie(w, &http.Cookie{Name: "tmp_token", Value: tmpToken, Path: "/", HttpOnly: true})
 69	http.SetCookie(w, &http.Cookie{Name: "tmp_secret", Value: tmpSecret, Path: "/", HttpOnly: true})
 70	http.Redirect(w, r, "https://www.tumblr.com/oauth/authorize?oauth_token="+tmpToken, http.StatusFound)
 71}
 72
 73func handleAuthCallback(w http.ResponseWriter, r *http.Request) {
 74	verifier := r.URL.Query().Get("oauth_verifier")
 75	if verifier == "" {
 76		http.Error(w, "Missing oauth_verifier", 400)
 77		return
 78	}
 79	tmpToken, err1 := r.Cookie("tmp_token")
 80	tmpSecret, err2 := r.Cookie("tmp_secret")
 81	if err1 != nil || err2 != nil {
 82		http.Error(w, "Missing temporary tokens", 400)
 83		return
 84	}
 85	consumerKey := os.Getenv("CONSUMER_KEY")
 86	consumerSecret := os.Getenv("CONSUMER_SECRET")
 87	permToken, permSecret, err := GetAccessToken(consumerKey, consumerSecret, tmpToken.Value, tmpSecret.Value, verifier)
 88	if err != nil {
 89		http.Error(w, "OAuth exchange failed: "+err.Error(), 500)
 90		return
 91	}
 92	http.SetCookie(w, &http.Cookie{Name: "perm_token", Value: permToken, Path: "/", HttpOnly: true})
 93	http.SetCookie(w, &http.Cookie{Name: "perm_secret", Value: permSecret, Path: "/", HttpOnly: true})
 94	http.Redirect(w, r, "/", http.StatusFound)
 95}
 96
 97func handleLogout(w http.ResponseWriter, r *http.Request) {
 98	http.SetCookie(w, &http.Cookie{Name: "perm_token", Value: "", MaxAge: -1, Path: "/"})
 99	http.SetCookie(w, &http.Cookie{Name: "perm_secret", Value: "", MaxAge: -1, Path: "/"})
100	http.Redirect(w, r, "/", http.StatusFound)
101}
102
103// --- Page handlers (render shell, JS does the loading) ---
104
105type PageData struct {
106	Title     string
107	BlogName  string
108	QueryOrBlog string
109	View      string // "dashboard" | "blog" | "search"
110}
111
112func handleGallery(w http.ResponseWriter, r *http.Request) {
113	token, secret, ok := requireAuth(w, r)
114	if !ok {
115		return
116	}
117	client := NewClient(os.Getenv("CONSUMER_KEY"), os.Getenv("CONSUMER_SECRET"), token, secret)
118	info, err := client.GetUserInfo()
119	blogName := ""
120	if err == nil && len(info.Response.User.Blogs) > 0 {
121		blogName = info.Response.User.Blogs[0].Name
122	}
123	tmpl.Execute(w, PageData{Title: "Michelangelo", BlogName: blogName, View: "dashboard"})
124}
125
126func handleBlog(w http.ResponseWriter, r *http.Request) {
127	_, _, ok := requireAuth(w, r)
128	if !ok {
129		return
130	}
131	name := strings.TrimPrefix(r.URL.Path, "/blog/")
132	name = strings.Trim(name, "/")
133	if name == "" {
134		http.Redirect(w, r, "/", http.StatusFound)
135		return
136	}
137	tmpl.Execute(w, PageData{Title: name + " — Michelangelo", QueryOrBlog: name, View: "blog"})
138}
139
140func handleSearch(w http.ResponseWriter, r *http.Request) {
141	_, _, ok := requireAuth(w, r)
142	if !ok {
143		return
144	}
145	q := r.URL.Query().Get("q")
146	tmpl.Execute(w, PageData{Title: "Search — Michelangelo", QueryOrBlog: q, View: "search"})
147}
148
149// --- API handlers (JSON, consumed by frontend) ---
150
151func jsonError(w http.ResponseWriter, msg string, code int) {
152	w.Header().Set("Content-Type", "application/json")
153	w.WriteHeader(code)
154	fmt.Fprintf(w, `{"error":%q}`, msg)
155}
156
157func getClient(w http.ResponseWriter, r *http.Request) (*TumblrClient, bool) {
158	token, secret, ok := requireAuth(w, r)
159	if !ok {
160		return nil, false
161	}
162	return NewClient(os.Getenv("CONSUMER_KEY"), os.Getenv("CONSUMER_SECRET"), token, secret), true
163}
164
165func handleAPIDashboard(w http.ResponseWriter, r *http.Request) {
166	client, ok := getClient(w, r)
167	if !ok {
168		return
169	}
170	offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
171	postType := r.URL.Query().Get("type")
172	if postType == "" {
173		postType = "photo"
174	}
175	posts, err := client.GetDashboard(offset, 20, postType)
176	if err != nil {
177		jsonError(w, err.Error(), 500)
178		return
179	}
180	w.Header().Set("Content-Type", "application/json")
181	json.NewEncoder(w).Encode(posts)
182}
183
184func handleAPIBlog(w http.ResponseWriter, r *http.Request) {
185	client, ok := getClient(w, r)
186	if !ok {
187		return
188	}
189	name := strings.TrimPrefix(r.URL.Path, "/api/blog/")
190	name = strings.Trim(name, "/")
191	offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
192	postType := r.URL.Query().Get("type")
193	if postType == "" {
194		postType = "photo"
195	}
196	posts, err := client.GetBlogPosts(name, offset, 20, postType)
197	if err != nil {
198		jsonError(w, err.Error(), 500)
199		return
200	}
201	w.Header().Set("Content-Type", "application/json")
202	json.NewEncoder(w).Encode(posts)
203}
204
205func handleAPISearch(w http.ResponseWriter, r *http.Request) {
206	client, ok := getClient(w, r)
207	if !ok {
208		return
209	}
210	q := r.URL.Query().Get("q")
211	if q == "" {
212		jsonError(w, "missing query", 400)
213		return
214	}
215	posts, err := client.GetTagged(q)
216	if err != nil {
217		jsonError(w, err.Error(), 500)
218		return
219	}
220	w.Header().Set("Content-Type", "application/json")
221	json.NewEncoder(w).Encode(posts)
222}
223
224func handleAPILike(w http.ResponseWriter, r *http.Request) {
225	client, ok := getClient(w, r)
226	if !ok {
227		return
228	}
229	id := r.URL.Query().Get("id")
230	key := r.URL.Query().Get("key")
231	unlike := r.URL.Query().Get("unlike") == "1"
232	var err error
233	if unlike {
234		err = client.UnlikePost(id, key)
235	} else {
236		err = client.LikePost(id, key)
237	}
238	if err != nil {
239		jsonError(w, err.Error(), 500)
240		return
241	}
242	w.Header().Set("Content-Type", "application/json")
243	fmt.Fprint(w, `{"ok":true}`)
244}
245
246func handleAPIReblog(w http.ResponseWriter, r *http.Request) {
247	client, ok := getClient(w, r)
248	if !ok {
249		return
250	}
251	if err := r.ParseForm(); err != nil {
252		jsonError(w, "bad request", 400)
253		return
254	}
255	blogName := r.FormValue("blog_name")
256	id := r.FormValue("id")
257	key := r.FormValue("reblog_key")
258	nativeBlog := r.FormValue("native_blog")
259	err := client.ReblogPost(nativeBlog, id, key, blogName)
260	if err != nil {
261		jsonError(w, err.Error(), 500)
262		return
263	}
264	w.Header().Set("Content-Type", "application/json")
265	fmt.Fprint(w, `{"ok":true}`)
266}