Refactor renderer and templates
[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 var (
19         ctx       = context.Background()
20         cookieAge = "31536000"
21 )
22
23 func NewHandler(s Service, staticDir string) http.Handler {
24         r := mux.NewRouter()
25
26         r.PathPrefix("/static").Handler(http.StripPrefix("/static",
27                 http.FileServer(http.Dir(path.Join(".", staticDir)))))
28
29         r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
30                 location := "/signin"
31
32                 sessionID, _ := req.Cookie("session_id")
33                 if sessionID != nil && len(sessionID.Value) > 0 {
34                         location = "/timeline/home"
35                 }
36
37                 w.Header().Add("Location", location)
38                 w.WriteHeader(http.StatusFound)
39         }).Methods(http.MethodGet)
40
41         r.HandleFunc("/signin", func(w http.ResponseWriter, req *http.Request) {
42                 err := s.ServeSigninPage(ctx, w)
43                 if err != nil {
44                         s.ServeErrorPage(ctx, w, nil, err)
45                         return
46                 }
47         }).Methods(http.MethodGet)
48
49         r.HandleFunc("/signin", func(w http.ResponseWriter, req *http.Request) {
50                 instance := req.FormValue("instance")
51                 url, sessionID, err := s.GetAuthUrl(ctx, instance)
52                 if err != nil {
53                         s.ServeErrorPage(ctx, w, nil, err)
54                         return
55                 }
56
57                 http.SetCookie(w, &http.Cookie{
58                         Name:    "session_id",
59                         Value:   sessionID,
60                         Expires: time.Now().Add(365 * 24 * time.Hour),
61                 })
62
63                 w.Header().Add("Location", url)
64                 w.WriteHeader(http.StatusFound)
65         }).Methods(http.MethodPost)
66
67         r.HandleFunc("/oauth_callback", func(w http.ResponseWriter, req *http.Request) {
68                 ctx := getContextWithSession(context.Background(), req)
69                 token := req.URL.Query().Get("code")
70                 _, err := s.GetUserToken(ctx, "", nil, token)
71                 if err != nil {
72                         s.ServeErrorPage(ctx, w, nil, err)
73                         return
74                 }
75
76                 w.Header().Add("Location", "/timeline/home")
77                 w.WriteHeader(http.StatusFound)
78         }).Methods(http.MethodGet)
79
80         r.HandleFunc("/timeline", func(w http.ResponseWriter, req *http.Request) {
81                 w.Header().Add("Location", "/timeline/home")
82                 w.WriteHeader(http.StatusFound)
83         }).Methods(http.MethodGet)
84
85         r.HandleFunc("/timeline/{type}", func(w http.ResponseWriter, req *http.Request) {
86                 ctx := getContextWithSession(context.Background(), req)
87
88                 timelineType, _ := mux.Vars(req)["type"]
89                 maxID := req.URL.Query().Get("max_id")
90                 sinceID := req.URL.Query().Get("since_id")
91                 minID := req.URL.Query().Get("min_id")
92
93                 err := s.ServeTimelinePage(ctx, w, nil, timelineType, maxID, sinceID, minID)
94                 if err != nil {
95                         s.ServeErrorPage(ctx, w, nil, err)
96                         return
97                 }
98         }).Methods(http.MethodGet)
99
100         r.HandleFunc("/thread/{id}", func(w http.ResponseWriter, req *http.Request) {
101                 ctx := getContextWithSession(context.Background(), req)
102                 id, _ := mux.Vars(req)["id"]
103                 reply := req.URL.Query().Get("reply")
104                 err := s.ServeThreadPage(ctx, w, nil, id, len(reply) > 1)
105                 if err != nil {
106                         s.ServeErrorPage(ctx, w, nil, err)
107                         return
108                 }
109         }).Methods(http.MethodGet)
110
111         r.HandleFunc("/likedby/{id}", func(w http.ResponseWriter, req *http.Request) {
112                 ctx := getContextWithSession(context.Background(), req)
113                 id, _ := mux.Vars(req)["id"]
114
115                 err := s.ServeLikedByPage(ctx, w, nil, id)
116                 if err != nil {
117                         s.ServeErrorPage(ctx, w, nil, err)
118                         return
119                 }
120         }).Methods(http.MethodGet)
121
122         r.HandleFunc("/retweetedby/{id}", func(w http.ResponseWriter, req *http.Request) {
123                 ctx := getContextWithSession(context.Background(), req)
124                 id, _ := mux.Vars(req)["id"]
125
126                 err := s.ServeRetweetedByPage(ctx, w, nil, id)
127                 if err != nil {
128                         s.ServeErrorPage(ctx, w, nil, err)
129                         return
130                 }
131         }).Methods(http.MethodGet)
132
133         r.HandleFunc("/following/{id}", func(w http.ResponseWriter, req *http.Request) {
134                 ctx := getContextWithSession(context.Background(), req)
135
136                 id, _ := mux.Vars(req)["id"]
137                 maxID := req.URL.Query().Get("max_id")
138                 minID := req.URL.Query().Get("min_id")
139
140                 err := s.ServeFollowingPage(ctx, w, nil, id, maxID, minID)
141                 if err != nil {
142                         s.ServeErrorPage(ctx, w, nil, err)
143                         return
144                 }
145         }).Methods(http.MethodGet)
146
147         r.HandleFunc("/followers/{id}", func(w http.ResponseWriter, req *http.Request) {
148                 ctx := getContextWithSession(context.Background(), req)
149
150                 id, _ := mux.Vars(req)["id"]
151                 maxID := req.URL.Query().Get("max_id")
152                 minID := req.URL.Query().Get("min_id")
153
154                 err := s.ServeFollowersPage(ctx, w, nil, id, maxID, minID)
155                 if err != nil {
156                         s.ServeErrorPage(ctx, w, nil, err)
157                         return
158                 }
159         }).Methods(http.MethodGet)
160
161         r.HandleFunc("/like/{id}", func(w http.ResponseWriter, req *http.Request) {
162                 ctx := getContextWithSession(context.Background(), req)
163                 id, _ := mux.Vars(req)["id"]
164                 retweetedByID := req.FormValue("retweeted_by_id")
165
166                 _, err := s.Like(ctx, w, nil, id)
167                 if err != nil {
168                         s.ServeErrorPage(ctx, w, nil, err)
169                         return
170                 }
171
172                 rID := id
173                 if len(retweetedByID) > 0 {
174                         rID = retweetedByID
175                 }
176                 w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
177                 w.WriteHeader(http.StatusFound)
178         }).Methods(http.MethodPost)
179
180         r.HandleFunc("/unlike/{id}", func(w http.ResponseWriter, req *http.Request) {
181                 ctx := getContextWithSession(context.Background(), req)
182                 id, _ := mux.Vars(req)["id"]
183                 retweetedByID := req.FormValue("retweeted_by_id")
184
185                 _, err := s.UnLike(ctx, w, nil, id)
186                 if err != nil {
187                         s.ServeErrorPage(ctx, w, nil, err)
188                         return
189                 }
190
191                 rID := id
192                 if len(retweetedByID) > 0 {
193                         rID = retweetedByID
194                 }
195                 w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
196                 w.WriteHeader(http.StatusFound)
197         }).Methods(http.MethodPost)
198
199         r.HandleFunc("/retweet/{id}", func(w http.ResponseWriter, req *http.Request) {
200                 ctx := getContextWithSession(context.Background(), req)
201                 id, _ := mux.Vars(req)["id"]
202                 retweetedByID := req.FormValue("retweeted_by_id")
203
204                 _, err := s.Retweet(ctx, w, nil, id)
205                 if err != nil {
206                         s.ServeErrorPage(ctx, w, nil, err)
207                         return
208                 }
209
210                 rID := id
211                 if len(retweetedByID) > 0 {
212                         rID = retweetedByID
213                 }
214                 w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
215                 w.WriteHeader(http.StatusFound)
216         }).Methods(http.MethodPost)
217
218         r.HandleFunc("/unretweet/{id}", func(w http.ResponseWriter, req *http.Request) {
219                 ctx := getContextWithSession(context.Background(), req)
220                 id, _ := mux.Vars(req)["id"]
221                 retweetedByID := req.FormValue("retweeted_by_id")
222
223                 _, err := s.UnRetweet(ctx, w, nil, id)
224                 if err != nil {
225                         s.ServeErrorPage(ctx, w, nil, err)
226                         return
227                 }
228
229                 rID := id
230                 if len(retweetedByID) > 0 {
231                         rID = retweetedByID
232                 }
233                 w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
234                 w.WriteHeader(http.StatusFound)
235         }).Methods(http.MethodPost)
236
237         r.HandleFunc("/fluoride/like/{id}", func(w http.ResponseWriter, req *http.Request) {
238                 ctx := getContextWithSession(context.Background(), req)
239                 id, _ := mux.Vars(req)["id"]
240                 count, err := s.Like(ctx, w, nil, id)
241                 if err != nil {
242                         s.ServeErrorPage(ctx, w, nil, err)
243                         return
244                 }
245
246                 err = serveJson(w, count)
247                 if err != nil {
248                         s.ServeErrorPage(ctx, w, nil, err)
249                         return
250                 }
251         }).Methods(http.MethodPost)
252
253         r.HandleFunc("/fluoride/unlike/{id}", func(w http.ResponseWriter, req *http.Request) {
254                 ctx := getContextWithSession(context.Background(), req)
255                 id, _ := mux.Vars(req)["id"]
256                 count, err := s.UnLike(ctx, w, nil, id)
257                 if err != nil {
258                         s.ServeErrorPage(ctx, w, nil, err)
259                         return
260                 }
261
262                 err = serveJson(w, count)
263                 if err != nil {
264                         s.ServeErrorPage(ctx, w, nil, err)
265                         return
266                 }
267         }).Methods(http.MethodPost)
268
269         r.HandleFunc("/fluoride/retweet/{id}", func(w http.ResponseWriter, req *http.Request) {
270                 ctx := getContextWithSession(context.Background(), req)
271                 id, _ := mux.Vars(req)["id"]
272                 count, err := s.Retweet(ctx, w, nil, id)
273                 if err != nil {
274                         s.ServeErrorPage(ctx, w, nil, err)
275                         return
276                 }
277
278                 err = serveJson(w, count)
279                 if err != nil {
280                         s.ServeErrorPage(ctx, w, nil, err)
281                         return
282                 }
283         }).Methods(http.MethodPost)
284
285         r.HandleFunc("/fluoride/unretweet/{id}", func(w http.ResponseWriter, req *http.Request) {
286                 ctx := getContextWithSession(context.Background(), req)
287                 id, _ := mux.Vars(req)["id"]
288                 count, err := s.UnRetweet(ctx, w, nil, id)
289                 if err != nil {
290                         s.ServeErrorPage(ctx, w, nil, err)
291                         return
292                 }
293
294                 err = serveJson(w, count)
295                 if err != nil {
296                         s.ServeErrorPage(ctx, w, nil, err)
297                         return
298                 }
299         }).Methods(http.MethodPost)
300
301         r.HandleFunc("/post", func(w http.ResponseWriter, req *http.Request) {
302                 ctx := getContextWithSession(context.Background(), req)
303
304                 err := req.ParseMultipartForm(4 << 20)
305                 if err != nil {
306                         s.ServeErrorPage(ctx, w, nil, err)
307                         return
308                 }
309
310                 content := getMultipartFormValue(req.MultipartForm, "content")
311                 replyToID := getMultipartFormValue(req.MultipartForm, "reply_to_id")
312                 format := getMultipartFormValue(req.MultipartForm, "format")
313                 visibility := getMultipartFormValue(req.MultipartForm, "visibility")
314                 isNSFW := "on" == getMultipartFormValue(req.MultipartForm, "is_nsfw")
315
316                 files := req.MultipartForm.File["attachments"]
317
318                 id, err := s.PostTweet(ctx, w, nil, content, replyToID, format, visibility, isNSFW, files)
319                 if err != nil {
320                         s.ServeErrorPage(ctx, w, nil, err)
321                         return
322                 }
323
324                 location := "/timeline/home" + "#status-" + id
325                 if len(replyToID) > 0 {
326                         location = "/thread/" + replyToID + "#status-" + id
327                 }
328                 w.Header().Add("Location", location)
329                 w.WriteHeader(http.StatusFound)
330         }).Methods(http.MethodPost)
331
332         r.HandleFunc("/notifications", func(w http.ResponseWriter, req *http.Request) {
333                 ctx := getContextWithSession(context.Background(), req)
334
335                 maxID := req.URL.Query().Get("max_id")
336                 minID := req.URL.Query().Get("min_id")
337
338                 err := s.ServeNotificationPage(ctx, w, nil, maxID, minID)
339                 if err != nil {
340                         s.ServeErrorPage(ctx, w, nil, err)
341                         return
342                 }
343         }).Methods(http.MethodGet)
344
345         r.HandleFunc("/user/{id}", func(w http.ResponseWriter, req *http.Request) {
346                 ctx := getContextWithSession(context.Background(), req)
347
348                 id, _ := mux.Vars(req)["id"]
349                 maxID := req.URL.Query().Get("max_id")
350                 minID := req.URL.Query().Get("min_id")
351
352                 err := s.ServeUserPage(ctx, w, nil, id, maxID, minID)
353                 if err != nil {
354                         s.ServeErrorPage(ctx, w, nil, err)
355                         return
356                 }
357         }).Methods(http.MethodGet)
358
359         r.HandleFunc("/follow/{id}", func(w http.ResponseWriter, req *http.Request) {
360                 ctx := getContextWithSession(context.Background(), req)
361
362                 id, _ := mux.Vars(req)["id"]
363
364                 err := s.Follow(ctx, w, nil, id)
365                 if err != nil {
366                         s.ServeErrorPage(ctx, w, nil, err)
367                         return
368                 }
369
370                 w.Header().Add("Location", req.Header.Get("Referer"))
371                 w.WriteHeader(http.StatusFound)
372         }).Methods(http.MethodPost)
373
374         r.HandleFunc("/unfollow/{id}", func(w http.ResponseWriter, req *http.Request) {
375                 ctx := getContextWithSession(context.Background(), req)
376
377                 id, _ := mux.Vars(req)["id"]
378
379                 err := s.UnFollow(ctx, w, nil, id)
380                 if err != nil {
381                         s.ServeErrorPage(ctx, w, nil, err)
382                         return
383                 }
384
385                 w.Header().Add("Location", req.Header.Get("Referer"))
386                 w.WriteHeader(http.StatusFound)
387         }).Methods(http.MethodPost)
388
389         r.HandleFunc("/about", func(w http.ResponseWriter, req *http.Request) {
390                 ctx := getContextWithSession(context.Background(), req)
391
392                 err := s.ServeAboutPage(ctx, w, nil)
393                 if err != nil {
394                         s.ServeErrorPage(ctx, w, nil, err)
395                         return
396                 }
397         }).Methods(http.MethodGet)
398
399         r.HandleFunc("/emojis", func(w http.ResponseWriter, req *http.Request) {
400                 ctx := getContextWithSession(context.Background(), req)
401
402                 err := s.ServeEmojiPage(ctx, w, nil)
403                 if err != nil {
404                         s.ServeErrorPage(ctx, w, nil, err)
405                         return
406                 }
407         }).Methods(http.MethodGet)
408
409         r.HandleFunc("/search", func(w http.ResponseWriter, req *http.Request) {
410                 ctx := getContextWithSession(context.Background(), req)
411
412                 q := req.URL.Query().Get("q")
413                 qType := req.URL.Query().Get("type")
414                 offsetStr := req.URL.Query().Get("offset")
415
416                 var offset int
417                 var err error
418                 if len(offsetStr) > 1 {
419                         offset, err = strconv.Atoi(offsetStr)
420                         if err != nil {
421                                 s.ServeErrorPage(ctx, w, nil, err)
422                                 return
423                         }
424                 }
425
426                 err = s.ServeSearchPage(ctx, w, nil, q, qType, offset)
427                 if err != nil {
428                         s.ServeErrorPage(ctx, w, nil, err)
429                         return
430                 }
431         }).Methods(http.MethodGet)
432
433         r.HandleFunc("/settings", func(w http.ResponseWriter, req *http.Request) {
434                 ctx := getContextWithSession(context.Background(), req)
435
436                 err := s.ServeSettingsPage(ctx, w, nil)
437                 if err != nil {
438                         s.ServeErrorPage(ctx, w, nil, err)
439                         return
440                 }
441         }).Methods(http.MethodGet)
442
443         r.HandleFunc("/settings", func(w http.ResponseWriter, req *http.Request) {
444                 ctx := getContextWithSession(context.Background(), req)
445
446                 visibility := req.FormValue("visibility")
447                 copyScope := req.FormValue("copy_scope") == "true"
448                 threadInNewTab := req.FormValue("thread_in_new_tab") == "true"
449                 maskNSFW := req.FormValue("mask_nsfw") == "true"
450                 fluorideMode := req.FormValue("fluoride_mode") == "true"
451                 darkMode := req.FormValue("dark_mode") == "true"
452                 settings := &model.Settings{
453                         DefaultVisibility: visibility,
454                         CopyScope:         copyScope,
455                         ThreadInNewTab:    threadInNewTab,
456                         MaskNSFW:          maskNSFW,
457                         FluorideMode:      fluorideMode,
458                         DarkMode:          darkMode,
459                 }
460
461                 err := s.SaveSettings(ctx, w, nil, settings)
462                 if err != nil {
463                         s.ServeErrorPage(ctx, w, nil, err)
464                         return
465                 }
466
467                 w.Header().Add("Location", req.Header.Get("Referer"))
468                 w.WriteHeader(http.StatusFound)
469         }).Methods(http.MethodPost)
470
471         r.HandleFunc("/signout", func(w http.ResponseWriter, req *http.Request) {
472                 // TODO remove session from database
473                 http.SetCookie(w, &http.Cookie{
474                         Name:    "session_id",
475                         Value:   "",
476                         Expires: time.Now(),
477                 })
478                 w.Header().Add("Location", "/")
479                 w.WriteHeader(http.StatusFound)
480         }).Methods(http.MethodGet)
481
482         return r
483 }
484
485 func getContextWithSession(ctx context.Context, req *http.Request) context.Context {
486         sessionID, err := req.Cookie("session_id")
487         if err != nil {
488                 return ctx
489         }
490         return context.WithValue(ctx, "session_id", sessionID.Value)
491 }
492
493 func getMultipartFormValue(mf *multipart.Form, key string) (val string) {
494         vals, ok := mf.Value[key]
495         if !ok {
496                 return ""
497         }
498         if len(vals) < 1 {
499                 return ""
500         }
501         return vals[0]
502 }
503
504 func serveJson(w io.Writer, data interface{}) (err error) {
505         var d = make(map[string]interface{})
506         d["data"] = data
507         return json.NewEncoder(w).Encode(d)
508 }