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}