Add conversation muting
[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         muteConversation := func(w http.ResponseWriter, req *http.Request) {
485                 c := newClient(w)
486                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
487                 id, _ := mux.Vars(req)["id"]
488
489                 err := s.MuteConversation(ctx, c, id)
490                 if err != nil {
491                         w.WriteHeader(http.StatusInternalServerError)
492                         s.ServeErrorPage(ctx, c, err)
493                         return
494                 }
495
496                 w.Header().Add("Location", req.Header.Get("Referer"))
497                 w.WriteHeader(http.StatusFound)
498         }
499
500         unMuteConversation := func(w http.ResponseWriter, req *http.Request) {
501                 c := newClient(w)
502                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
503                 id, _ := mux.Vars(req)["id"]
504
505                 err := s.UnMuteConversation(ctx, c, id)
506                 if err != nil {
507                         w.WriteHeader(http.StatusInternalServerError)
508                         s.ServeErrorPage(ctx, c, err)
509                         return
510                 }
511
512                 w.Header().Add("Location", req.Header.Get("Referer"))
513                 w.WriteHeader(http.StatusFound)
514         }
515
516         signout := func(w http.ResponseWriter, req *http.Request) {
517                 // TODO remove session from database
518                 http.SetCookie(w, &http.Cookie{
519                         Name:    "session_id",
520                         Value:   "",
521                         Expires: time.Now(),
522                 })
523                 w.Header().Add("Location", "/")
524                 w.WriteHeader(http.StatusFound)
525         }
526
527         fLike := func(w http.ResponseWriter, req *http.Request) {
528                 c := newClient(w)
529                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
530                 id, _ := mux.Vars(req)["id"]
531
532                 count, err := s.Like(ctx, c, id)
533                 if err != nil {
534                         serveJsonError(w, err)
535                         return
536                 }
537
538                 err = serveJson(w, count)
539                 if err != nil {
540                         serveJsonError(w, err)
541                         return
542                 }
543         }
544
545         fUnlike := func(w http.ResponseWriter, req *http.Request) {
546                 c := newClient(w)
547                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
548                 id, _ := mux.Vars(req)["id"]
549                 count, err := s.UnLike(ctx, c, id)
550                 if err != nil {
551                         serveJsonError(w, err)
552                         return
553                 }
554
555                 err = serveJson(w, count)
556                 if err != nil {
557                         serveJsonError(w, err)
558                         return
559                 }
560         }
561
562         fRetweet := func(w http.ResponseWriter, req *http.Request) {
563                 c := newClient(w)
564                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
565                 id, _ := mux.Vars(req)["id"]
566
567                 count, err := s.Retweet(ctx, c, id)
568                 if err != nil {
569                         serveJsonError(w, err)
570                         return
571                 }
572
573                 err = serveJson(w, count)
574                 if err != nil {
575                         serveJsonError(w, err)
576                         return
577                 }
578         }
579
580         fUnretweet := func(w http.ResponseWriter, req *http.Request) {
581                 c := newClient(w)
582                 ctx := newCtxWithSesionCSRF(req, req.FormValue("csrf_token"))
583                 id, _ := mux.Vars(req)["id"]
584
585                 count, err := s.UnRetweet(ctx, c, id)
586                 if err != nil {
587                         serveJsonError(w, err)
588                         return
589                 }
590
591                 err = serveJson(w, count)
592                 if err != nil {
593                         serveJsonError(w, err)
594                         return
595                 }
596         }
597
598         r.HandleFunc("/", rootPage).Methods(http.MethodGet)
599         r.HandleFunc("/signin", signinPage).Methods(http.MethodGet)
600         r.HandleFunc("/timeline/{type}", timelinePage).Methods(http.MethodGet)
601         r.HandleFunc("/timeline", timelineOldPage).Methods(http.MethodGet)
602         r.HandleFunc("/thread/{id}", threadPage).Methods(http.MethodGet)
603         r.HandleFunc("/likedby/{id}", likedByPage).Methods(http.MethodGet)
604         r.HandleFunc("/retweetedby/{id}", retweetedByPage).Methods(http.MethodGet)
605         r.HandleFunc("/notifications", notificationsPage).Methods(http.MethodGet)
606         r.HandleFunc("/user/{id}", userPage).Methods(http.MethodGet)
607         r.HandleFunc("/user/{id}/{type}", userPage).Methods(http.MethodGet)
608         r.HandleFunc("/usersearch/{id}", userSearchPage).Methods(http.MethodGet)
609         r.HandleFunc("/about", aboutPage).Methods(http.MethodGet)
610         r.HandleFunc("/emojis", emojisPage).Methods(http.MethodGet)
611         r.HandleFunc("/search", searchPage).Methods(http.MethodGet)
612         r.HandleFunc("/settings", settingsPage).Methods(http.MethodGet)
613         r.HandleFunc("/signin", signin).Methods(http.MethodPost)
614         r.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet)
615         r.HandleFunc("/post", post).Methods(http.MethodPost)
616         r.HandleFunc("/like/{id}", like).Methods(http.MethodPost)
617         r.HandleFunc("/unlike/{id}", unlike).Methods(http.MethodPost)
618         r.HandleFunc("/retweet/{id}", retweet).Methods(http.MethodPost)
619         r.HandleFunc("/unretweet/{id}", unretweet).Methods(http.MethodPost)
620         r.HandleFunc("/follow/{id}", follow).Methods(http.MethodPost)
621         r.HandleFunc("/unfollow/{id}", unfollow).Methods(http.MethodPost)
622         r.HandleFunc("/settings", settings).Methods(http.MethodPost)
623         r.HandleFunc("/muteconv/{id}", muteConversation).Methods(http.MethodPost)
624         r.HandleFunc("/unmuteconv/{id}", unMuteConversation).Methods(http.MethodPost)
625         r.HandleFunc("/signout", signout).Methods(http.MethodGet)
626         r.HandleFunc("/fluoride/like/{id}", fLike).Methods(http.MethodPost)
627         r.HandleFunc("/fluoride/unlike/{id}", fUnlike).Methods(http.MethodPost)
628         r.HandleFunc("/fluoride/retweet/{id}", fRetweet).Methods(http.MethodPost)
629         r.HandleFunc("/fluoride/unretweet/{id}", fUnretweet).Methods(http.MethodPost)
630         r.PathPrefix("/static").Handler(http.StripPrefix("/static",
631                 http.FileServer(http.Dir(staticDir))))
632
633         return r
634 }