d945e182f907c5725af457ddb935b4cefe295c01
[bloat] / service / transport.go
1 package service
2
3 import (
4         "context"
5         "encoding/json"
6         "io"
7         "mime/multipart"
8         "net/http"
9         "path"
10         "strconv"
11         "time"
12
13         "bloat/model"
14
15         "github.com/gorilla/mux"
16 )
17
18 func newClient(w io.Writer) *model.Client {
19         return &model.Client{
20                 Writer: w,
21         }
22 }
23
24 func newCtxWithSesion(req *http.Request) context.Context {
25         ctx := context.Background()
26         sessionID, err := req.Cookie("session_id")
27         if err != nil {
28                 return ctx
29         }
30         return context.WithValue(ctx, "session_id", sessionID.Value)
31 }
32
33 func newCtxWithSesionCSRF(req *http.Request, csrfToken string) context.Context {
34         ctx := newCtxWithSesion(req)
35         return context.WithValue(ctx, "csrf_token", csrfToken)
36 }
37
38 func getMultipartFormValue(mf *multipart.Form, key string) (val string) {
39         vals, ok := mf.Value[key]
40         if !ok {
41                 return ""
42         }
43         if len(vals) < 1 {
44                 return ""
45         }
46         return vals[0]
47 }
48
49 func serveJson(w io.Writer, data interface{}) (err error) {
50         var d = make(map[string]interface{})
51         d["data"] = data
52         return json.NewEncoder(w).Encode(d)
53 }
54
55 func serveJsonError(w http.ResponseWriter, err error) {
56         var d = make(map[string]interface{})
57         d["error"] = err.Error()
58         w.WriteHeader(http.StatusInternalServerError)
59         json.NewEncoder(w).Encode(d)
60         return
61 }
62
63 func NewHandler(s Service, staticDir string) http.Handler {
64         r := mux.NewRouter()
65
66         rootPage := func(w http.ResponseWriter, req *http.Request) {
67                 sessionID, _ := req.Cookie("session_id")
68
69                 location := "/signin"
70                 if sessionID != nil && len(sessionID.Value) > 0 {
71                         location = "/timeline/home"
72                 }
73
74                 w.Header().Add("Location", location)
75                 w.WriteHeader(http.StatusFound)
76         }
77
78         signinPage := func(w http.ResponseWriter, req *http.Request) {
79                 c := newClient(w)
80                 ctx := context.Background()
81                 err := s.ServeSigninPage(ctx, c)
82                 if err != nil {
83                         w.WriteHeader(http.StatusInternalServerError)
84                         s.ServeErrorPage(ctx, c, err)
85                         return
86                 }
87         }
88
89         timelinePage := func(w http.ResponseWriter, req *http.Request) {
90                 c := newClient(w)
91                 ctx := newCtxWithSesion(req)
92                 tType, _ := mux.Vars(req)["type"]
93                 maxID := req.URL.Query().Get("max_id")
94                 minID := req.URL.Query().Get("min_id")
95
96                 err := s.ServeTimelinePage(ctx, c, tType, maxID, minID)
97                 if err != nil {
98                         w.WriteHeader(http.StatusInternalServerError)
99                         s.ServeErrorPage(ctx, c, err)
100                         return
101                 }
102         }
103
104         timelineOldPage := func(w http.ResponseWriter, req *http.Request) {
105                 w.Header().Add("Location", "/timeline/home")
106                 w.WriteHeader(http.StatusFound)
107         }
108
109         threadPage := func(w http.ResponseWriter, req *http.Request) {
110                 c := newClient(w)
111                 ctx := newCtxWithSesion(req)
112                 id, _ := mux.Vars(req)["id"]
113                 reply := req.URL.Query().Get("reply")
114
115                 err := s.ServeThreadPage(ctx, c, id, len(reply) > 1)
116                 if err != nil {
117                         w.WriteHeader(http.StatusInternalServerError)
118                         s.ServeErrorPage(ctx, c, err)
119                         return
120                 }
121         }
122
123         likedByPage := func(w http.ResponseWriter, req *http.Request) {
124                 c := newClient(w)
125                 ctx := newCtxWithSesion(req)
126                 id, _ := mux.Vars(req)["id"]
127
128                 err := s.ServeLikedByPage(ctx, c, id)
129                 if err != nil {
130                         w.WriteHeader(http.StatusInternalServerError)
131                         s.ServeErrorPage(ctx, c, err)
132                         return
133                 }
134         }
135
136         retweetedByPage := func(w http.ResponseWriter, req *http.Request) {
137                 c := newClient(w)
138                 ctx := newCtxWithSesion(req)
139                 id, _ := mux.Vars(req)["id"]
140
141                 err := s.ServeRetweetedByPage(ctx, c, id)
142                 if err != nil {
143                         w.WriteHeader(http.StatusInternalServerError)
144                         s.ServeErrorPage(ctx, c, err)
145                         return
146                 }
147         }
148
149         notificationsPage := func(w http.ResponseWriter, req *http.Request) {
150                 c := newClient(w)
151                 ctx := newCtxWithSesion(req)
152                 maxID := req.URL.Query().Get("max_id")
153                 minID := req.URL.Query().Get("min_id")
154
155                 err := s.ServeNotificationPage(ctx, c, maxID, minID)
156                 if err != nil {
157                         w.WriteHeader(http.StatusInternalServerError)
158                         s.ServeErrorPage(ctx, c, err)
159                         return
160                 }
161         }
162
163         userPage := func(w http.ResponseWriter, req *http.Request) {
164                 c := newClient(w)
165                 ctx := newCtxWithSesion(req)
166                 id, _ := mux.Vars(req)["id"]
167                 pageType, _ := mux.Vars(req)["type"]
168                 maxID := req.URL.Query().Get("max_id")
169                 minID := req.URL.Query().Get("min_id")
170
171                 err := s.ServeUserPage(ctx, c, id, pageType, maxID, minID)
172                 if err != nil {
173                         w.WriteHeader(http.StatusInternalServerError)
174                         s.ServeErrorPage(ctx, c, err)
175                         return
176                 }
177         }
178
179         userSearchPage := func(w http.ResponseWriter, req *http.Request) {
180                 c := newClient(w)
181                 ctx := newCtxWithSesion(req)
182                 id, _ := mux.Vars(req)["id"]
183                 q := req.URL.Query().Get("q")
184                 offsetStr := req.URL.Query().Get("offset")
185
186                 var offset int
187                 var err error
188                 if len(offsetStr) > 1 {
189                         offset, err = strconv.Atoi(offsetStr)
190                         if err != nil {
191                                 w.WriteHeader(http.StatusInternalServerError)
192                                 s.ServeErrorPage(ctx, c, err)
193                                 return
194                         }
195                 }
196
197                 err = s.ServeUserSearchPage(ctx, c, id, q, offset)
198                 if err != nil {
199                         w.WriteHeader(http.StatusInternalServerError)
200                         s.ServeErrorPage(ctx, c, err)
201                         return
202                 }
203         }
204
205         aboutPage := func(w http.ResponseWriter, req *http.Request) {
206                 c := newClient(w)
207                 ctx := newCtxWithSesion(req)
208
209                 err := s.ServeAboutPage(ctx, c)
210                 if err != nil {
211                         w.WriteHeader(http.StatusInternalServerError)
212                         s.ServeErrorPage(ctx, c, err)
213                         return
214                 }
215         }
216
217         emojisPage := func(w http.ResponseWriter, req *http.Request) {
218                 c := newClient(w)
219                 ctx := newCtxWithSesion(req)
220
221                 err := s.ServeEmojiPage(ctx, c)
222                 if err != nil {
223                         w.WriteHeader(http.StatusInternalServerError)
224                         s.ServeErrorPage(ctx, c, err)
225                         return
226                 }
227         }
228
229         searchPage := func(w http.ResponseWriter, req *http.Request) {
230                 c := newClient(w)
231                 ctx := newCtxWithSesion(req)
232                 q := req.URL.Query().Get("q")
233                 qType := req.URL.Query().Get("type")
234                 offsetStr := req.URL.Query().Get("offset")
235
236                 var offset int
237                 var err error
238                 if len(offsetStr) > 1 {
239                         offset, err = strconv.Atoi(offsetStr)
240                         if err != nil {
241                                 w.WriteHeader(http.StatusInternalServerError)
242                                 s.ServeErrorPage(ctx, c, err)
243                                 return
244                         }
245                 }
246
247                 err = s.ServeSearchPage(ctx, c, q, qType, offset)
248                 if err != nil {
249                         w.WriteHeader(http.StatusInternalServerError)
250                         s.ServeErrorPage(ctx, c, err)
251                         return
252                 }
253         }
254
255         settingsPage := func(w http.ResponseWriter, req *http.Request) {
256                 c := newClient(w)
257                 ctx := newCtxWithSesion(req)
258
259                 err := s.ServeSettingsPage(ctx, c)
260                 if err != nil {
261                         w.WriteHeader(http.StatusInternalServerError)
262                         s.ServeErrorPage(ctx, c, err)
263                         return
264                 }
265         }
266
267         signin := func(w http.ResponseWriter, req *http.Request) {
268                 c := newClient(w)
269                 ctx := context.Background()
270                 instance := req.FormValue("instance")
271
272                 url, sessionID, err := s.NewSession(ctx, instance)
273                 if err != nil {
274                         w.WriteHeader(http.StatusInternalServerError)
275                         s.ServeErrorPage(ctx, c, err)
276                         return
277                 }
278
279                 http.SetCookie(w, &http.Cookie{
280                         Name:    "session_id",
281                         Value:   sessionID,
282                         Expires: time.Now().Add(365 * 24 * time.Hour),
283                 })
284
285                 w.Header().Add("Location", url)
286                 w.WriteHeader(http.StatusFound)
287         }
288
289         oauthCallback := func(w http.ResponseWriter, req *http.Request) {
290                 c := newClient(w)
291                 ctx := newCtxWithSesion(req)
292                 token := req.URL.Query().Get("code")
293
294                 _, err := s.Signin(ctx, c, "", token)
295                 if err != nil {
296                         w.WriteHeader(http.StatusInternalServerError)
297                         s.ServeErrorPage(ctx, c, err)
298                         return
299                 }
300
301                 w.Header().Add("Location", "/timeline/home")
302                 w.WriteHeader(http.StatusFound)
303         }
304
305         post := func(w http.ResponseWriter, req *http.Request) {
306                 c := newClient(w)
307                 err := req.ParseMultipartForm(4 << 20)
308                 if err != nil {
309                         w.WriteHeader(http.StatusInternalServerError)
310                         s.ServeErrorPage(context.Background(), c, err)
311                         return
312                 }
313
314                 ctx := newCtxWithSesionCSRF(req,
315                         getMultipartFormValue(req.MultipartForm, "csrf_token"))
316                 content := getMultipartFormValue(req.MultipartForm, "content")
317                 replyToID := getMultipartFormValue(req.MultipartForm, "reply_to_id")
318                 format := getMultipartFormValue(req.MultipartForm, "format")
319                 visibility := getMultipartFormValue(req.MultipartForm, "visibility")
320                 isNSFW := "on" == getMultipartFormValue(req.MultipartForm, "is_nsfw")
321                 files := req.MultipartForm.File["attachments"]
322
323                 id, err := s.Post(ctx, c, content, replyToID, format, visibility, isNSFW, files)
324                 if err != nil {
325                         w.WriteHeader(http.StatusInternalServerError)
326                         s.ServeErrorPage(ctx, c, err)
327                         return
328                 }
329
330                 location := "/timeline/home" + "#status-" + id
331                 if len(replyToID) > 0 {
332                         location = "/thread/" + replyToID + "#status-" + id
333                 }
334                 w.Header().Add("Location", location)
335                 w.WriteHeader(http.StatusFound)
336         }
337
338         like := func(w http.ResponseWriter, req *http.Request) {
339                 c := newClient(w)
340                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
341                 id, _ := mux.Vars(req)["id"]
342                 retweetedByID := req.FormValue("retweeted_by_id")
343
344                 _, err := s.Like(ctx, c, id)
345                 if err != nil {
346                         w.WriteHeader(http.StatusInternalServerError)
347                         s.ServeErrorPage(ctx, c, err)
348                         return
349                 }
350
351                 rID := id
352                 if len(retweetedByID) > 0 {
353                         rID = retweetedByID
354                 }
355                 w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
356                 w.WriteHeader(http.StatusFound)
357         }
358
359         unlike := func(w http.ResponseWriter, req *http.Request) {
360                 c := newClient(w)
361                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
362                 id, _ := mux.Vars(req)["id"]
363                 retweetedByID := req.FormValue("retweeted_by_id")
364
365                 _, err := s.UnLike(ctx, c, id)
366                 if err != nil {
367                         w.WriteHeader(http.StatusInternalServerError)
368                         s.ServeErrorPage(ctx, c, err)
369                         return
370                 }
371
372                 rID := id
373                 if len(retweetedByID) > 0 {
374                         rID = retweetedByID
375                 }
376                 w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
377                 w.WriteHeader(http.StatusFound)
378         }
379
380         retweet := func(w http.ResponseWriter, req *http.Request) {
381                 c := newClient(w)
382                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
383                 id, _ := mux.Vars(req)["id"]
384                 retweetedByID := req.FormValue("retweeted_by_id")
385
386                 _, err := s.Retweet(ctx, c, id)
387                 if err != nil {
388                         w.WriteHeader(http.StatusInternalServerError)
389                         s.ServeErrorPage(ctx, c, err)
390                         return
391                 }
392
393                 rID := id
394                 if len(retweetedByID) > 0 {
395                         rID = retweetedByID
396                 }
397                 w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
398                 w.WriteHeader(http.StatusFound)
399         }
400
401         unretweet := func(w http.ResponseWriter, req *http.Request) {
402                 c := newClient(w)
403                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
404                 id, _ := mux.Vars(req)["id"]
405                 retweetedByID := req.FormValue("retweeted_by_id")
406
407                 _, err := s.UnRetweet(ctx, c, id)
408                 if err != nil {
409                         w.WriteHeader(http.StatusInternalServerError)
410                         s.ServeErrorPage(ctx, c, err)
411                         return
412                 }
413
414                 rID := id
415                 if len(retweetedByID) > 0 {
416                         rID = retweetedByID
417                 }
418
419                 w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
420                 w.WriteHeader(http.StatusFound)
421         }
422
423         follow := func(w http.ResponseWriter, req *http.Request) {
424                 c := newClient(w)
425                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
426                 id, _ := mux.Vars(req)["id"]
427
428                 err := s.Follow(ctx, c, id)
429                 if err != nil {
430                         w.WriteHeader(http.StatusInternalServerError)
431                         s.ServeErrorPage(ctx, c, err)
432                         return
433                 }
434
435                 w.Header().Add("Location", req.Header.Get("Referer"))
436                 w.WriteHeader(http.StatusFound)
437         }
438
439         unfollow := func(w http.ResponseWriter, req *http.Request) {
440                 c := newClient(w)
441                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
442                 id, _ := mux.Vars(req)["id"]
443
444                 err := s.UnFollow(ctx, c, id)
445                 if err != nil {
446                         w.WriteHeader(http.StatusInternalServerError)
447                         s.ServeErrorPage(ctx, c, err)
448                         return
449                 }
450
451                 w.Header().Add("Location", req.Header.Get("Referer"))
452                 w.WriteHeader(http.StatusFound)
453         }
454
455         settings := func(w http.ResponseWriter, req *http.Request) {
456                 c := newClient(w)
457                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
458                 visibility := req.FormValue("visibility")
459                 copyScope := req.FormValue("copy_scope") == "true"
460                 threadInNewTab := req.FormValue("thread_in_new_tab") == "true"
461                 maskNSFW := req.FormValue("mask_nsfw") == "true"
462                 fluorideMode := req.FormValue("fluoride_mode") == "true"
463                 darkMode := req.FormValue("dark_mode") == "true"
464
465                 settings := &model.Settings{
466                         DefaultVisibility: visibility,
467                         CopyScope:         copyScope,
468                         ThreadInNewTab:    threadInNewTab,
469                         MaskNSFW:          maskNSFW,
470                         FluorideMode:      fluorideMode,
471                         DarkMode:          darkMode,
472                 }
473
474                 err := s.SaveSettings(ctx, c, settings)
475                 if err != nil {
476                         w.WriteHeader(http.StatusInternalServerError)
477                         s.ServeErrorPage(ctx, c, err)
478                         return
479                 }
480
481                 w.Header().Add("Location", req.Header.Get("Referer"))
482                 w.WriteHeader(http.StatusFound)
483         }
484
485         signout := func(w http.ResponseWriter, req *http.Request) {
486                 // TODO remove session from database
487                 http.SetCookie(w, &http.Cookie{
488                         Name:    "session_id",
489                         Value:   "",
490                         Expires: time.Now(),
491                 })
492                 w.Header().Add("Location", "/")
493                 w.WriteHeader(http.StatusFound)
494         }
495
496         fLike := func(w http.ResponseWriter, req *http.Request) {
497                 c := newClient(w)
498                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
499                 id, _ := mux.Vars(req)["id"]
500
501                 count, err := s.Like(ctx, c, id)
502                 if err != nil {
503                         serveJsonError(w, err)
504                         return
505                 }
506
507                 err = serveJson(w, count)
508                 if err != nil {
509                         serveJsonError(w, err)
510                         return
511                 }
512         }
513
514         fUnlike := func(w http.ResponseWriter, req *http.Request) {
515                 c := newClient(w)
516                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
517                 id, _ := mux.Vars(req)["id"]
518                 count, err := s.UnLike(ctx, c, id)
519                 if err != nil {
520                         serveJsonError(w, err)
521                         return
522                 }
523
524                 err = serveJson(w, count)
525                 if err != nil {
526                         serveJsonError(w, err)
527                         return
528                 }
529         }
530
531         fRetweet := func(w http.ResponseWriter, req *http.Request) {
532                 c := newClient(w)
533                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
534                 id, _ := mux.Vars(req)["id"]
535
536                 count, err := s.Retweet(ctx, c, id)
537                 if err != nil {
538                         serveJsonError(w, err)
539                         return
540                 }
541
542                 err = serveJson(w, count)
543                 if err != nil {
544                         serveJsonError(w, err)
545                         return
546                 }
547         }
548
549         fUnretweet := func(w http.ResponseWriter, req *http.Request) {
550                 c := newClient(w)
551                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
552                 id, _ := mux.Vars(req)["id"]
553
554                 count, err := s.UnRetweet(ctx, c, id)
555                 if err != nil {
556                         serveJsonError(w, err)
557                         return
558                 }
559
560                 err = serveJson(w, count)
561                 if err != nil {
562                         serveJsonError(w, err)
563                         return
564                 }
565         }
566
567         r.HandleFunc("/", rootPage).Methods(http.MethodGet)
568         r.HandleFunc("/signin", signinPage).Methods(http.MethodGet)
569         r.HandleFunc("/timeline/{type}", timelinePage).Methods(http.MethodGet)
570         r.HandleFunc("/timeline", timelineOldPage).Methods(http.MethodGet)
571         r.HandleFunc("/thread/{id}", threadPage).Methods(http.MethodGet)
572         r.HandleFunc("/likedby/{id}", likedByPage).Methods(http.MethodGet)
573         r.HandleFunc("/retweetedby/{id}", retweetedByPage).Methods(http.MethodGet)
574         r.HandleFunc("/notifications", notificationsPage).Methods(http.MethodGet)
575         r.HandleFunc("/user/{id}", userPage).Methods(http.MethodGet)
576         r.HandleFunc("/user/{id}/{type}", userPage).Methods(http.MethodGet)
577         r.HandleFunc("/usersearch/{id}", userSearchPage).Methods(http.MethodGet)
578         r.HandleFunc("/about", aboutPage).Methods(http.MethodGet)
579         r.HandleFunc("/emojis", emojisPage).Methods(http.MethodGet)
580         r.HandleFunc("/search", searchPage).Methods(http.MethodGet)
581         r.HandleFunc("/settings", settingsPage).Methods(http.MethodGet)
582         r.HandleFunc("/signin", signin).Methods(http.MethodPost)
583         r.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet)
584         r.HandleFunc("/post", post).Methods(http.MethodPost)
585         r.HandleFunc("/like/{id}", like).Methods(http.MethodPost)
586         r.HandleFunc("/unlike/{id}", unlike).Methods(http.MethodPost)
587         r.HandleFunc("/retweet/{id}", retweet).Methods(http.MethodPost)
588         r.HandleFunc("/unretweet/{id}", unretweet).Methods(http.MethodPost)
589         r.HandleFunc("/follow/{id}", follow).Methods(http.MethodPost)
590         r.HandleFunc("/unfollow/{id}", unfollow).Methods(http.MethodPost)
591         r.HandleFunc("/settings", settings).Methods(http.MethodPost)
592         r.HandleFunc("/signout", signout).Methods(http.MethodGet)
593         r.HandleFunc("/fluoride/like/{id}", fLike).Methods(http.MethodPost)
594         r.HandleFunc("/fluoride/unlike/{id}", fUnlike).Methods(http.MethodPost)
595         r.HandleFunc("/fluoride/retweet/{id}", fRetweet).Methods(http.MethodPost)
596         r.HandleFunc("/fluoride/unretweet/{id}", fUnretweet).Methods(http.MethodPost)
597         r.PathPrefix("/static").Handler(http.StripPrefix("/static",
598                 http.FileServer(http.Dir(path.Join(".", staticDir)))))
599
600         return r
601 }