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