pastes.sh/internal/api.go

502 lines
12 KiB
Go

package internal
import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"time"
"git.sr.ht/~erock/wish/cms/db"
"git.sr.ht/~erock/wish/cms/db/postgres"
)
type PageData struct {
Site SitePageData
}
type PostItemData struct {
URL template.URL
BlogURL template.URL
Username string
Title string
Description string
PublishAtISO string
PublishAt string
UpdatedAtISO string
UpdatedTimeAgo string
Padding string
}
type BlogPageData struct {
Site SitePageData
PageTitle string
URL template.URL
RSSURL template.URL
Username string
Header *HeaderTxt
Posts []PostItemData
}
type ReadPageData struct {
Site SitePageData
NextPage string
PrevPage string
Posts []PostItemData
}
type PostPageData struct {
Site SitePageData
PageTitle string
URL template.URL
BlogURL template.URL
Title string
Description string
Username string
BlogName string
Contents template.HTML
PublishAtISO string
PublishAt string
}
type TransparencyPageData struct {
Site SitePageData
Analytics *db.Analytics
}
func renderTemplate(templates []string) (*template.Template, error) {
files := make([]string, len(templates))
copy(files, templates)
files = append(
files,
"./html/footer.partial.tmpl",
"./html/marketing-footer.partial.tmpl",
"./html/base.layout.tmpl",
)
ts, err := template.ParseFiles(files...)
if err != nil {
return nil, err
}
return ts, nil
}
func createPageHandler(fname string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := GetLogger(r)
cfg := GetCfg(r)
ts, err := renderTemplate([]string{fname})
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := PageData{
Site: *cfg.GetSiteData(),
}
err = ts.Execute(w, data)
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
type Link struct {
URL string
Text string
}
type HeaderTxt struct {
Title string
Bio string
Nav []Link
HasLinks bool
}
type ReadmeTxt struct {
HasText bool
Contents template.HTML
}
func GetUsernameFromRequest(r *http.Request) string {
subdomain := GetSubdomain(r)
cfg := GetCfg(r)
if !cfg.IsSubdomains() || subdomain == "" {
return GetField(r, 0)
}
return subdomain
}
func blogHandler(w http.ResponseWriter, r *http.Request) {
username := GetUsernameFromRequest(r)
dbpool := GetDB(r)
logger := GetLogger(r)
cfg := GetCfg(r)
user, err := dbpool.FindUserForName(username)
if err != nil {
logger.Infof("blog not found: %s", username)
http.Error(w, "blog not found", http.StatusNotFound)
return
}
posts, err := dbpool.FindPostsForUser(user.ID)
if err != nil {
logger.Error(err)
http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
return
}
ts, err := renderTemplate([]string{
"./html/blog.page.tmpl",
})
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
headerTxt := &HeaderTxt{
Title: GetBlogName(username),
Bio: "",
}
postCollection := make([]PostItemData, 0, len(posts))
for _, post := range posts {
p := PostItemData{
URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
BlogURL: template.URL(cfg.BlogURL(post.Username)),
Title: FilenameToTitle(post.Filename, post.Title),
PublishAt: post.PublishAt.Format("02 Jan, 2006"),
PublishAtISO: post.PublishAt.Format(time.RFC3339),
UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
}
postCollection = append(postCollection, p)
}
data := BlogPageData{
Site: *cfg.GetSiteData(),
PageTitle: headerTxt.Title,
URL: template.URL(cfg.BlogURL(username)),
RSSURL: template.URL(cfg.RssBlogURL(username)),
Header: headerTxt,
Username: username,
Posts: postCollection,
}
err = ts.Execute(w, data)
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func GetPostTitle(post *db.Post) string {
if post.Description == "" {
return post.Title
}
return fmt.Sprintf("%s: %s", post.Title, post.Description)
}
func GetBlogName(username string) string {
return fmt.Sprintf("%s's blog", username)
}
func postHandler(w http.ResponseWriter, r *http.Request) {
username := GetUsernameFromRequest(r)
subdomain := GetSubdomain(r)
cfg := GetCfg(r)
var filename string
if !cfg.IsSubdomains() || subdomain == "" {
filename, _ = url.PathUnescape(GetField(r, 1))
} else {
filename, _ = url.PathUnescape(GetField(r, 0))
}
dbpool := GetDB(r)
logger := GetLogger(r)
user, err := dbpool.FindUserForName(username)
if err != nil {
logger.Infof("blog not found: %s", username)
http.Error(w, "blog not found", http.StatusNotFound)
return
}
blogName := GetBlogName(username)
post, err := dbpool.FindPostWithFilename(filename, user.ID)
if err != nil {
logger.Infof("post not found %s/%s", username, filename)
http.Error(w, "post not found", http.StatusNotFound)
return
}
parsedText, err := ParseText(post.Filename, post.Text)
if err != nil {
logger.Error(err)
}
data := PostPageData{
Site: *cfg.GetSiteData(),
PageTitle: GetPostTitle(post),
URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
BlogURL: template.URL(cfg.BlogURL(username)),
Description: post.Description,
Title: FilenameToTitle(post.Filename, post.Title),
PublishAt: post.PublishAt.Format("02 Jan, 2006"),
PublishAtISO: post.PublishAt.Format(time.RFC3339),
Username: username,
BlogName: blogName,
Contents: template.HTML(parsedText),
}
ts, err := renderTemplate([]string{
"./html/post.page.tmpl",
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = ts.Execute(w, data)
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
username := GetUsernameFromRequest(r)
subdomain := GetSubdomain(r)
cfg := GetCfg(r)
var filename string
if !cfg.IsSubdomains() || subdomain == "" {
filename, _ = url.PathUnescape(GetField(r, 1))
} else {
filename, _ = url.PathUnescape(GetField(r, 0))
}
dbpool := GetDB(r)
logger := GetLogger(r)
user, err := dbpool.FindUserForName(username)
if err != nil {
logger.Infof("blog not found: %s", username)
http.Error(w, "blog not found", http.StatusNotFound)
return
}
post, err := dbpool.FindPostWithFilename(filename, user.ID)
if err != nil {
logger.Infof("post not found %s/%s", username, filename)
http.Error(w, "post not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(post.Text))
}
func transparencyHandler(w http.ResponseWriter, r *http.Request) {
dbpool := GetDB(r)
logger := GetLogger(r)
cfg := GetCfg(r)
analytics, err := dbpool.FindSiteAnalytics()
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ts, err := template.ParseFiles(
"./html/transparency.page.tmpl",
"./html/footer.partial.tmpl",
"./html/marketing-footer.partial.tmpl",
"./html/base.layout.tmpl",
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
data := TransparencyPageData{
Site: *cfg.GetSiteData(),
Analytics: analytics,
}
err = ts.Execute(w, data)
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func readHandler(w http.ResponseWriter, r *http.Request) {
dbpool := GetDB(r)
logger := GetLogger(r)
cfg := GetCfg(r)
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
pager, err := dbpool.FindAllPosts(&db.Pager{Num: 30, Page: page})
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ts, err := renderTemplate([]string{
"./html/read.page.tmpl",
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
nextPage := ""
if page < pager.Total-1 {
nextPage = fmt.Sprintf("/read?page=%d", page+1)
}
prevPage := ""
if page > 0 {
prevPage = fmt.Sprintf("/read?page=%d", page-1)
}
data := ReadPageData{
Site: *cfg.GetSiteData(),
NextPage: nextPage,
PrevPage: prevPage,
}
for _, post := range pager.Data {
item := PostItemData{
URL: template.URL(cfg.PostURL(post.Username, post.Filename)),
BlogURL: template.URL(cfg.BlogURL(post.Username)),
Title: FilenameToTitle(post.Filename, post.Title),
Description: post.Description,
Username: post.Username,
PublishAt: post.PublishAt.Format("02 Jan, 2006"),
PublishAtISO: post.PublishAt.Format(time.RFC3339),
UpdatedTimeAgo: TimeAgo(post.UpdatedAt),
UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
}
data.Posts = append(data.Posts, item)
}
err = ts.Execute(w, data)
if err != nil {
logger.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func serveFile(file string, contentType string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := GetLogger(r)
contents, err := ioutil.ReadFile(fmt.Sprintf("./public/%s", file))
if err != nil {
logger.Error(err)
http.Error(w, "file not found", 404)
}
w.Header().Add("Content-Type", contentType)
_, err = w.Write(contents)
if err != nil {
logger.Error(err)
http.Error(w, "server error", 500)
}
}
}
func createStaticRoutes() []Route {
return []Route{
NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
}
}
func createMainRoutes(staticRoutes []Route) []Route {
routes := []Route{
NewRoute("GET", "/", createPageHandler("./html/marketing.page.tmpl")),
NewRoute("GET", "/spec", createPageHandler("./html/spec.page.tmpl")),
NewRoute("GET", "/ops", createPageHandler("./html/ops.page.tmpl")),
NewRoute("GET", "/privacy", createPageHandler("./html/privacy.page.tmpl")),
NewRoute("GET", "/help", createPageHandler("./html/help.page.tmpl")),
NewRoute("GET", "/transparency", transparencyHandler),
NewRoute("GET", "/read", readHandler),
}
routes = append(
routes,
staticRoutes...,
)
routes = append(
routes,
NewRoute("GET", "/([^/]+)", blogHandler),
NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
NewRoute("GET", "/([^/]+)/([^/]+)/raw", postHandlerRaw),
)
return routes
}
func createSubdomainRoutes(staticRoutes []Route) []Route {
routes := []Route{
NewRoute("GET", "/", blogHandler),
}
routes = append(
routes,
staticRoutes...,
)
routes = append(
routes,
NewRoute("GET", "/([^/]+)", postHandler),
NewRoute("GET", "/([^/]+)/raw", postHandlerRaw),
)
return routes
}
func StartApiServer() {
cfg := NewConfigSite()
db := postgres.NewDB(&cfg.ConfigCms)
defer db.Close()
logger := cfg.Logger
go CronDeleteExpiredPosts(cfg, db)
staticRoutes := createStaticRoutes()
mainRoutes := createMainRoutes(staticRoutes)
subdomainRoutes := createSubdomainRoutes(staticRoutes)
handler := CreateServe(mainRoutes, subdomainRoutes, cfg, db, logger)
router := http.HandlerFunc(handler)
portStr := fmt.Sprintf(":%s", cfg.Port)
logger.Infof("Starting server on port %s", cfg.Port)
logger.Infof("Subdomains enabled: %t", cfg.SubdomainsEnabled)
logger.Infof("Domain: %s", cfg.Domain)
logger.Infof("Email: %s", cfg.Email)
logger.Fatal(http.ListenAndServe(portStr, router))
}