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