tiny 88x31 lexicon for atproto
at main 454 lines 15 kB view raw
1package handler 2 3import ( 4 "fmt" 5 "github.com/bluesky-social/indigo/atproto/auth/oauth" 6 "github.com/bluesky-social/indigo/atproto/identity" 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "github.com/gorilla/sessions" 9 "html/template" 10 "log" 11 "net/http" 12 "os" 13 "strconv" 14 "tangled.org/moth11.net/88x31/db" 15 myoauth "tangled.org/moth11.net/88x31/oauth" 16 "tangled.org/moth11.net/88x31/types" 17 "time" 18) 19 20type Handler struct { 21 db *db.Store 22 router *http.ServeMux 23 oauth *myoauth.Service 24 sessionStore *sessions.CookieStore 25} 26 27var fm = template.FuncMap{ 28 "deref": func(in *string) string { 29 if in == nil { 30 return "" 31 } 32 return *in 33 }, 34} 35 36var buttonT = template.Must(template.New("beeper").Funcs(fm).ParseFiles("./tmpl/partial/buttonpart.html", "./tmpl/base.html", "./tmpl/button.html")) 37var homeT = template.Must(template.ParseFiles("./tmpl/partial/buttonpart.html", "./tmpl/base.html", "./tmpl/home.html")) 38var loginT = template.Must(template.ParseFiles("./tmpl/base.html", "./tmpl/login.html")) 39var logoutT = template.Must(template.ParseFiles("./tmpl/base.html", "./tmpl/logout.html")) 40var uploadT = template.Must(template.ParseFiles("./tmpl/base.html", "./tmpl/upload.html")) 41var referenceT = template.Must(template.ParseFiles("./tmpl/base.html", "./tmpl/reference.html")) 42var lbreferenceT = template.Must(template.ParseFiles("./tmpl/base.html", "./tmpl/lbreference.html", "./tmpl/partial/apidoc.html", "./tmpl/partial/iframe.html")) 43var embedT = template.Must(template.ParseFiles("./tmpl/partial/embedbuttonpart.html", "./tmpl/embedbase.html", "./tmpl/embedcollection.html")) 44 45func MakeHandler(db *db.Store, oauth *myoauth.Service) *Handler { 46 mux := http.NewServeMux() 47 sessionStore := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) 48 h := &Handler{db: db, router: mux, oauth: oauth, sessionStore: sessionStore} 49 mux.HandleFunc("GET /", h.oauthMiddleware(h.gethome)) 50 mux.HandleFunc("GET /reference", h.oauthMiddleware(h.getreference)) 51 mux.HandleFunc("GET /embed/liked-by/reference", h.oauthMiddleware(h.getlbreference)) 52 mux.HandleFunc("GET /login", h.oauthMiddleware(getlogin)) 53 mux.HandleFunc("POST /login", h.login) 54 mux.HandleFunc("GET /logout", h.oauthMiddleware(getlogout)) 55 mux.HandleFunc("POST /logout", h.oauthMiddleware(h.logout)) 56 mux.HandleFunc("GET /upload", h.oauthMiddleware(getupload)) 57 mux.HandleFunc("POST /upload", h.oauthMiddleware(h.upload)) 58 mux.HandleFunc("POST /delete", h.oauthMiddleware(h.delete)) 59 mux.HandleFunc("POST /like", h.oauthMiddleware(h.like)) 60 mux.HandleFunc("POST /unlike", h.oauthMiddleware(h.unlike)) 61 mux.HandleFunc("GET /button", h.oauthMiddleware(h.getbutton)) 62 mux.HandleFunc("GET /xrpc/store.88x31.getButton", h.WithCORS(h.getButton)) 63 mux.HandleFunc("GET /xrpc/store.88x31.getButtons", h.WithCORS(h.getButtons)) 64 mux.HandleFunc("GET /xrpc/store.88x31.getLikedButtons", h.WithCORS(h.getLikedButtons)) 65 mux.HandleFunc("GET /embed/liked-by", h.WithCORS(h.getuserlikes)) 66 mux.HandleFunc(oauthCallbackPath(), h.oauthCallback) 67 mux.HandleFunc(clientMetadataPath(), h.WithCORS(h.serveClientMetadata)) 68 mux.HandleFunc(oauthJWKSPath(), h.WithCORS(h.serveJWKS)) 69 mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { 70 http.ServeFile(w, r, "./static/favicon.ico") 71 }) 72 return h 73} 74 75type EZData struct { 76 DID *string 77 Title string 78} 79 80func getlogin(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 81 if cs != nil { 82 http.Redirect(w, r, "/logout", http.StatusSeeOther) 83 } 84 var ezd EZData 85 ezd.Title = "login" 86 err := loginT.ExecuteTemplate(w, "base.html", ezd) 87 if err != nil { 88 http.Error(w, err.Error(), http.StatusInternalServerError) 89 } 90} 91 92func getlogout(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 93 if cs == nil { 94 http.Redirect(w, r, "/login", http.StatusSeeOther) 95 } 96 var ezd EZData 97 did := cs.Data.AccountDID.String() 98 ezd.DID = &did 99 ezd.Title = "logout" 100 err := logoutT.ExecuteTemplate(w, "base.html", ezd) 101 if err != nil { 102 http.Error(w, err.Error(), http.StatusInternalServerError) 103 } 104} 105 106func getupload(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 107 var ezd EZData 108 if cs != nil { 109 did := cs.Data.AccountDID.String() 110 ezd.DID = &did 111 } 112 ezd.Title = "upload" 113 err := uploadT.ExecuteTemplate(w, "base.html", ezd) 114 if err != nil { 115 http.Error(w, err.Error(), http.StatusInternalServerError) 116 } 117} 118 119func (h *Handler) gethome(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 120 btnView, _, err := h.db.GetButtons(50, nil, r.Context()) 121 if err != nil || len(btnView) == 0 { 122 log.Println(err) 123 btnView = nil 124 } 125 type HomeData struct { 126 DID *string 127 Title string 128 Buttons []types.ButtonView 129 } 130 var hd HomeData 131 if cs != nil { 132 did := cs.Data.AccountDID.String() 133 hd.DID = &did 134 } 135 hd.Title = "upload" 136 hd.Buttons = btnView 137 138 err = homeT.ExecuteTemplate(w, "base.html", hd) 139 if err != nil { 140 http.Error(w, err.Error(), http.StatusInternalServerError) 141 } 142} 143func (h *Handler) getreference(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 144 type ReferenceData struct { 145 DID *string 146 Title string 147 } 148 var rd ReferenceData 149 if cs != nil { 150 did := cs.Data.AccountDID.String() 151 rd.DID = &did 152 } 153 rd.Title = "reference" 154 155 err := referenceT.ExecuteTemplate(w, "base.html", rd) 156 if err != nil { 157 http.Error(w, err.Error(), http.StatusInternalServerError) 158 } 159} 160 161func (h *Handler) getlbreference(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 162 type Apidoc struct { 163 Fragment string 164 Accepts string 165 Notes string 166 HREF string 167 } 168 type ReferenceData struct { 169 DID *string 170 Title string 171 Docs []Apidoc 172 } 173 var rd ReferenceData 174 if cs != nil { 175 did := cs.Data.AccountDID.String() 176 rd.DID = &did 177 } 178 rd.Title = "reference" 179 rd.Docs = []Apidoc{ 180 { 181 "did=", 182 "atproto did as string", 183 "prefer to use atproto did! i don't locally store handle:did mappings, so if you use a handle, i have to resolve it on the fly, which adds latency", 184 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce", 185 }, 186 { 187 "handle=", 188 "atproto handle as string", 189 "prefer to use atproto did! i don't locally store handle:did mappings, so if you use a handle, i have to resolve it on the fly, which adds latency", 190 "https://88x31.store/embed/liked-by?handle=moth11.net", 191 }, 192 { 193 "limit=", 194 "integer as string", 195 "min=1, max=100, fetches up to limit liked buttons reverse chronologically", 196 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&limit=2", 197 }, 198 { 199 "attribute=", 200 "non-empty string", 201 "any non-empty string for attribute will add a line at the bottom of the generated html linking to 88x31.store. no worries if you prefer the plain look!", 202 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&attribute=yes", 203 }, 204 { 205 "image-rendering=", 206 "any css image-rendering option: 'auto', 'smooth', 'high-quality', 'pixelated', 'crisp-edges'. default 'pixelated'", 207 "this is the image-rendering option used, prefer pixelated (default, so you can leave it unset if you don't have any opinions here) if you want the pixel art to look crisp. compare the output from this example with smooth to initial-size='s, which uses default pixelated", 208 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&initial-size=2&image-rendering=smooth", 209 }, 210 { 211 "initial-size=", 212 "float as string", 213 "prefer an integer for better scaling of pixel art", 214 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&initial-size=2", 215 }, 216 { 217 "hover-scale=", 218 "float as string", 219 "this is the scale factor that the button shifts to when a user hovers over it. ex: initial-size=2 and hover-scale=3 means that the final width when user hovers will be 88*2*3=528px", 220 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&hover-scale=1.1", 221 }, 222 { 223 "hover-rotate=", 224 "float degrees as string", 225 "this is the number of degrees that the button rotates when a user hovers over it", 226 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&hover-rotate=31", 227 }, 228 { 229 "margin=", 230 "float pixels as string", 231 "this is the iframe's internal margin", 232 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&margin=20", 233 }, 234 { 235 "margin-bottom=", 236 "float pixels as string", 237 "this is the size of the bottom margin for each button", 238 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&margin-bottom=20", 239 }, 240 { 241 "margin-right=", 242 "float pixels as string", 243 "this is the size of the right margin for each button", 244 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&margin-right=20", 245 }, 246 { 247 "cache=", 248 "int seconds as string", 249 "this is how long the embed should be cached for. whenever you load a page with an iframe, first you make the html request to get the page, and then once your browser parses the page, it normally makes a second request to populate the iframe. this means when you load a page with an iframe, the page loads first, and the iframe loads second, so there's a slight flicker. if you set the cache fragment, your browser will cache the contents of the iframe's html, so the iframe will feel more like it's really part of the webpage. however, if the state of the user's collection changes before the cache expires, then those changes won't be visible. this is unrelated to how long the buttons themselves are cached for (1 year)", 250 "https://88x31.store/embed/liked-by?did=did:plc:25z6ogppprfvijcnqo2fsfce&cache=600", 251 }, 252 } 253 254 err := lbreferenceT.ExecuteTemplate(w, "base.html", rd) 255 if err != nil { 256 http.Error(w, err.Error(), http.StatusInternalServerError) 257 } 258} 259 260func (h *Handler) getuserlikes(w http.ResponseWriter, r *http.Request) { 261 q := r.URL.Query() 262 did := q.Get("did") 263 handle := r.URL.Query().Get("handle") 264 if did == "" && handle != "" { 265 h, err := syntax.ParseHandle(handle) 266 if err == nil { 267 id, err := identity.DefaultDirectory().LookupHandle(r.Context(), h) 268 if err == nil { 269 did = id.DID.String() 270 } 271 } 272 } 273 limit := q.Get("limit") 274 lint, err := strconv.Atoi(limit) 275 if err != nil { 276 lint = 50 277 } 278 if lint < 1 { 279 lint = 1 280 } 281 if lint > 100 { 282 lint = 100 283 } 284 cursor := q.Get("cursor") 285 var crsr *string 286 if cursor != "" { 287 crsr = &cursor 288 } 289 attribute := q.Get("attribute") 290 initialsize := q.Get("initial-size") 291 isize, err := strconv.ParseFloat(initialsize, 64) 292 var tisize *float64 293 if err == nil { 294 tisize = &isize 295 } 296 hoverscale := q.Get("hover-scale") 297 hsize, err := strconv.ParseFloat(hoverscale, 64) 298 var thsize *float64 299 if err == nil { 300 thsize = &hsize 301 } 302 hoverrotate := q.Get("hover-rotate") 303 hrot, err := strconv.ParseFloat(hoverrotate, 64) 304 var throt *float64 305 if err == nil { 306 throt = &hrot 307 } 308 mr := q.Get("margin-right") 309 marginright, err := strconv.ParseFloat(mr, 64) 310 if err != nil { 311 marginright = 2 312 } 313 mb := q.Get("margin-bottom") 314 marginbottom, err := strconv.ParseFloat(mb, 64) 315 if err != nil { 316 marginbottom = 2 317 } 318 m := q.Get("margin") 319 margin, err := strconv.ParseFloat(m, 64) 320 if err != nil { 321 margin = 0 322 } 323 imageRendering := q.Get("image-rendering") 324 switch imageRendering { 325 case "auto", "smooth", "crisp-edges", "high-quality": 326 break 327 default: 328 imageRendering = "pixelated" 329 } 330 btns, ncrsr, err := h.db.GetUserLikes(did, lint, crsr, r.Context()) 331 if err != nil { 332 log.Println(err) 333 http.Error(w, "error", http.StatusInternalServerError) 334 return 335 } 336 type EmbedData struct { 337 Margin float64 338 Buttons []types.ButtonView 339 Cursor *time.Time 340 Attribute bool 341 ImageRendering string 342 InitialSize *float64 343 HoverCSS bool 344 HoverScale *float64 345 HoverRotate *float64 346 MarginRight float64 347 MarginBottom float64 348 } 349 var ed EmbedData 350 ed.Margin = margin 351 ed.Buttons = btns 352 ed.Cursor = ncrsr 353 ed.Attribute = attribute != "" 354 ed.ImageRendering = imageRendering 355 ed.InitialSize = tisize 356 ed.HoverScale = thsize 357 ed.HoverRotate = throt 358 ed.HoverCSS = throt != nil || thsize != nil 359 ed.MarginRight = marginright 360 ed.MarginBottom = marginbottom 361 cache := q.Get("cache") 362 cacheage, err := strconv.Atoi(cache) 363 if err == nil { 364 w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%d, immutable", cacheage)) 365 } 366 err = embedT.ExecuteTemplate(w, "embedbase.html", ed) 367 if err != nil { 368 log.Println(err) 369 http.Error(w, "error templating", http.StatusInternalServerError) 370 } 371} 372 373func (h *Handler) getbutton(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 374 if cs != nil { 375 h.getbuttonauth(cs, w, r) 376 return 377 } 378 uri := r.URL.Query().Get("uri") 379 btn, err := h.db.GetButton(uri, r.Context()) 380 if err != nil || btn == nil { 381 http.Error(w, "not found", http.StatusNotFound) 382 return 383 } 384 type ButtonData struct { 385 DID *string 386 Title string 387 Button types.Button 388 } 389 var btnd ButtonData 390 if btn.Alt != nil { 391 btnd.Title = *btn.Alt 392 } else { 393 btnd.Title = uri 394 } 395 btnd.Button = *btn 396 err = buttonT.ExecuteTemplate(w, "base.html", btnd) 397 if err != nil { 398 http.Error(w, "error templating", http.StatusInternalServerError) 399 } 400} 401 402func (h *Handler) getbuttonauth(cs *oauth.ClientSession, w http.ResponseWriter, r *http.Request) { 403 if cs == nil { 404 panic("cs must be non nil when i call getbuttonauth") 405 } 406 did := cs.Data.AccountDID.String() 407 uri := r.URL.Query().Get("uri") 408 btn, err := h.db.GetButtonAuth(uri, r.Context(), did) 409 if err != nil || btn == nil { 410 if err != nil { 411 log.Println(err) 412 } 413 http.Error(w, "not found", http.StatusNotFound) 414 return 415 } 416 type ButtonData struct { 417 DID *string 418 Title string 419 Button types.ButtonViewAuth 420 } 421 var btnd ButtonData 422 btnd.DID = &did 423 if btn.Alt != nil { 424 btnd.Title = *btn.Alt 425 } else { 426 btnd.Title = uri 427 } 428 btnd.Button = *btn 429 err = buttonT.ExecuteTemplate(w, "base.html", btnd) 430 if err != nil { 431 log.Println(err) 432 http.Error(w, "error templating", http.StatusInternalServerError) 433 } 434} 435 436func (h *Handler) Serve() http.Handler { 437 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 438 h.router.ServeHTTP(w, r) 439 }) 440} 441 442func (h *Handler) WithCORS(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { 443 return func(w http.ResponseWriter, r *http.Request) { 444 w.Header().Set("Access-Control-Allow-Origin", "*") 445 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 446 w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorizaton, X-Requested-With, Sec-WebSocket-Protocol, Sec-WebSocket-Extensions, Sec-WebSocket-Key, Sec-WebSocket-Version") 447 w.Header().Set("Access-Control-Allow-Credentials", "true") 448 if r.Method == "Options" { 449 w.WriteHeader(http.StatusOK) 450 return 451 } 452 f(w, r) 453 } 454}