Eric Bower
·
26 Jun 24
api.go
1package pastes
2
3import (
4 "fmt"
5 "html/template"
6 "net/http"
7 "net/url"
8 "os"
9 "time"
10
11 "github.com/picosh/pico/db"
12 "github.com/picosh/pico/db/postgres"
13 "github.com/picosh/pico/shared"
14 "github.com/picosh/pico/shared/storage"
15)
16
17type PageData struct {
18 Site shared.SitePageData
19}
20
21type PostItemData struct {
22 URL template.URL
23 BlogURL template.URL
24 Username string
25 Title string
26 Description string
27 PublishAtISO string
28 PublishAt string
29 UpdatedAtISO string
30 UpdatedTimeAgo string
31 Padding string
32}
33
34type BlogPageData struct {
35 Site shared.SitePageData
36 PageTitle string
37 URL template.URL
38 RSSURL template.URL
39 Username string
40 Header *HeaderTxt
41 Posts []PostItemData
42}
43
44type PostPageData struct {
45 Site shared.SitePageData
46 PageTitle string
47 URL template.URL
48 RawURL template.URL
49 BlogURL template.URL
50 Title string
51 Description string
52 Username string
53 BlogName string
54 Contents template.HTML
55 PublishAtISO string
56 PublishAt string
57 ExpiresAt string
58 Unlisted bool
59}
60
61type TransparencyPageData struct {
62 Site shared.SitePageData
63 Analytics *db.Analytics
64}
65
66type Link struct {
67 URL string
68 Text string
69}
70
71type HeaderTxt struct {
72 Title string
73 Bio string
74 Nav []Link
75 HasLinks bool
76}
77
78func blogHandler(w http.ResponseWriter, r *http.Request) {
79 username := shared.GetUsernameFromRequest(r)
80 dbpool := shared.GetDB(r)
81 logger := shared.GetLogger(r)
82 cfg := shared.GetCfg(r)
83
84 user, err := dbpool.FindUserForName(username)
85 if err != nil {
86 logger.Info("blog not found", "user", username)
87 http.Error(w, "blog not found", http.StatusNotFound)
88 return
89 }
90 pager, err := dbpool.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, user.ID, cfg.Space)
91 posts := pager.Data
92
93 if err != nil {
94 logger.Error(err.Error())
95 http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
96 return
97 }
98
99 ts, err := shared.RenderTemplate(cfg, []string{
100 cfg.StaticPath("html/blog.page.tmpl"),
101 })
102
103 if err != nil {
104 logger.Error(err.Error())
105 http.Error(w, err.Error(), http.StatusInternalServerError)
106 return
107 }
108
109 headerTxt := &HeaderTxt{
110 Title: GetBlogName(username),
111 Bio: "",
112 }
113
114 curl := shared.CreateURLFromRequest(cfg, r)
115 postCollection := make([]PostItemData, 0, len(posts))
116 for _, post := range posts {
117 p := PostItemData{
118 URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
119 BlogURL: template.URL(cfg.FullBlogURL(curl, post.Username)),
120 Title: post.Filename,
121 PublishAt: post.PublishAt.Format(time.DateOnly),
122 PublishAtISO: post.PublishAt.Format(time.RFC3339),
123 UpdatedTimeAgo: shared.TimeAgo(post.UpdatedAt),
124 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
125 }
126 postCollection = append(postCollection, p)
127 }
128
129 data := BlogPageData{
130 Site: *cfg.GetSiteData(),
131 PageTitle: headerTxt.Title,
132 URL: template.URL(cfg.FullBlogURL(curl, username)),
133 RSSURL: template.URL(cfg.RssBlogURL(curl, username, "")),
134 Header: headerTxt,
135 Username: username,
136 Posts: postCollection,
137 }
138
139 err = ts.Execute(w, data)
140 if err != nil {
141 logger.Error(err.Error())
142 http.Error(w, err.Error(), http.StatusInternalServerError)
143 }
144}
145
146func GetPostTitle(post *db.Post) string {
147 if post.Description == "" {
148 return post.Title
149 }
150
151 return fmt.Sprintf("%s: %s", post.Title, post.Description)
152}
153
154func GetBlogName(username string) string {
155 return fmt.Sprintf("%s's pastes", username)
156}
157
158func postHandler(w http.ResponseWriter, r *http.Request) {
159 username := shared.GetUsernameFromRequest(r)
160 subdomain := shared.GetSubdomain(r)
161 cfg := shared.GetCfg(r)
162
163 var slug string
164 if !cfg.IsSubdomains() || subdomain == "" {
165 slug, _ = url.PathUnescape(shared.GetField(r, 1))
166 } else {
167 slug, _ = url.PathUnescape(shared.GetField(r, 0))
168 }
169
170 dbpool := shared.GetDB(r)
171 logger := shared.GetLogger(r)
172
173 user, err := dbpool.FindUserForName(username)
174 if err != nil {
175 logger.Info("blog not found", "user", username)
176 http.Error(w, "blog not found", http.StatusNotFound)
177 return
178 }
179
180 blogName := GetBlogName(username)
181
182 var data PostPageData
183 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
184 if err == nil {
185 parsedText, err := ParseText(post.Filename, post.Text)
186 if err != nil {
187 logger.Error(err.Error())
188 }
189 expiresAt := "never"
190 if post.ExpiresAt != nil {
191 expiresAt = post.ExpiresAt.Format(time.DateOnly)
192 }
193
194 unlisted := false
195 if post.Hidden {
196 unlisted = true
197 }
198
199 data = PostPageData{
200 Site: *cfg.GetSiteData(),
201 PageTitle: post.Filename,
202 URL: template.URL(cfg.PostURL(post.Username, post.Slug)),
203 RawURL: template.URL(cfg.RawPostURL(post.Username, post.Slug)),
204 BlogURL: template.URL(cfg.BlogURL(username)),
205 Description: post.Description,
206 Title: post.Filename,
207 PublishAt: post.PublishAt.Format(time.DateOnly),
208 PublishAtISO: post.PublishAt.Format(time.RFC3339),
209 Username: username,
210 BlogName: blogName,
211 Contents: template.HTML(parsedText),
212 ExpiresAt: expiresAt,
213 Unlisted: unlisted,
214 }
215 } else {
216 logger.Info("post not found", "user", username, "slug", slug)
217 data = PostPageData{
218 Site: *cfg.GetSiteData(),
219 PageTitle: "Paste not found",
220 Description: "Paste not found",
221 Title: "Paste not found",
222 BlogURL: template.URL(cfg.BlogURL(username)),
223 PublishAt: time.Now().Format(time.DateOnly),
224 PublishAtISO: time.Now().Format(time.RFC3339),
225 Username: username,
226 BlogName: blogName,
227 Contents: "oops! we can't seem to find this post.",
228 ExpiresAt: "",
229 }
230 }
231
232 ts, err := shared.RenderTemplate(cfg, []string{
233 cfg.StaticPath("html/post.page.tmpl"),
234 })
235
236 if err != nil {
237 http.Error(w, err.Error(), http.StatusInternalServerError)
238 }
239
240 err = ts.Execute(w, data)
241 if err != nil {
242 logger.Error(err.Error())
243 http.Error(w, err.Error(), http.StatusInternalServerError)
244 }
245}
246
247func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
248 username := shared.GetUsernameFromRequest(r)
249 subdomain := shared.GetSubdomain(r)
250 cfg := shared.GetCfg(r)
251
252 var slug string
253 if !cfg.IsSubdomains() || subdomain == "" {
254 slug, _ = url.PathUnescape(shared.GetField(r, 1))
255 } else {
256 slug, _ = url.PathUnescape(shared.GetField(r, 0))
257 }
258
259 dbpool := shared.GetDB(r)
260 logger := shared.GetLogger(r)
261
262 user, err := dbpool.FindUserForName(username)
263 if err != nil {
264 logger.Info("blog not found", "user", username)
265 http.Error(w, "blog not found", http.StatusNotFound)
266 return
267 }
268
269 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
270 if err != nil {
271 logger.Info("post not found", "user", username, "slug", slug)
272 http.Error(w, "post not found", http.StatusNotFound)
273 return
274 }
275
276 w.Header().Set("Content-Type", "text/plain")
277 _, err = w.Write([]byte(post.Text))
278 if err != nil {
279 logger.Error(err.Error())
280 }
281}
282
283func serveFile(file string, contentType string) http.HandlerFunc {
284 return func(w http.ResponseWriter, r *http.Request) {
285 logger := shared.GetLogger(r)
286 cfg := shared.GetCfg(r)
287
288 contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
289 if err != nil {
290 logger.Error(err.Error())
291 http.Error(w, "file not found", 404)
292 }
293 w.Header().Add("Content-Type", contentType)
294
295 _, err = w.Write(contents)
296 if err != nil {
297 logger.Error(err.Error())
298 http.Error(w, "server error", 500)
299 }
300 }
301}
302
303func createStaticRoutes() []shared.Route {
304 return []shared.Route{
305 shared.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
306 shared.NewRoute("GET", "/smol.css", serveFile("smol.css", "text/css")),
307 shared.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
308 shared.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
309 shared.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
310 shared.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
311 shared.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
312 shared.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
313 shared.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
314 }
315}
316
317func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
318 routes := []shared.Route{
319 shared.NewRoute("GET", "/", shared.CreatePageHandler("html/marketing.page.tmpl")),
320 shared.NewRoute("GET", "/check", shared.CheckHandler),
321 }
322
323 routes = append(
324 routes,
325 staticRoutes...,
326 )
327
328 routes = append(
329 routes,
330 shared.NewRoute("GET", "/([^/]+)", blogHandler),
331 shared.NewRoute("GET", "/([^/]+)/([^/]+)", postHandler),
332 shared.NewRoute("GET", "/([^/]+)/([^/]+)/raw", postHandlerRaw),
333 shared.NewRoute("GET", "/raw/([^/]+)/([^/]+)", postHandlerRaw),
334 )
335
336 return routes
337}
338
339func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
340 routes := []shared.Route{
341 shared.NewRoute("GET", "/", blogHandler),
342 }
343
344 routes = append(
345 routes,
346 staticRoutes...,
347 )
348
349 routes = append(
350 routes,
351 shared.NewRoute("GET", "/([^/]+)", postHandler),
352 shared.NewRoute("GET", "/([^/]+)/raw", postHandlerRaw),
353 shared.NewRoute("GET", "/raw/([^/]+)", postHandlerRaw),
354 )
355
356 return routes
357}
358
359func StartApiServer() {
360 cfg := NewConfigSite()
361 db := postgres.NewDB(cfg.DbURL, cfg.Logger)
362 defer db.Close()
363 logger := cfg.Logger
364
365 var st storage.StorageServe
366 var err error
367 if cfg.MinioURL == "" {
368 st, err = storage.NewStorageFS(cfg.StorageDir)
369 } else {
370 st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
371 }
372
373 if err != nil {
374 logger.Error(err.Error())
375 return
376 }
377
378 go CronDeleteExpiredPosts(cfg, db)
379
380 staticRoutes := createStaticRoutes()
381
382 if cfg.Debug {
383 staticRoutes = shared.CreatePProfRoutes(staticRoutes)
384 }
385
386 mainRoutes := createMainRoutes(staticRoutes)
387 subdomainRoutes := createSubdomainRoutes(staticRoutes)
388
389 apiConfig := &shared.ApiConfig{
390 Cfg: cfg,
391 Dbpool: db,
392 Storage: st,
393 }
394 handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
395 router := http.HandlerFunc(handler)
396
397 portStr := fmt.Sprintf(":%s", cfg.Port)
398 logger.Info(
399 "Starting server on port",
400 "port", cfg.Port,
401 "domain", cfg.Domain,
402 )
403
404 logger.Error(http.ListenAndServe(portStr, router).Error())
405}