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