Add bloatfe-tan.
[bloat] / service / transport.go
1 package service
2
3 import (
4         "encoding/json"
5         "errors"
6         "log"
7         "net/http"
8         "strconv"
9         "time"
10
11         "bloat/mastodon"
12         "bloat/model"
13
14         "github.com/gorilla/mux"
15 )
16
17 var (
18         errInvalidSession   = errors.New("invalid session")
19         errInvalidCSRFToken = errors.New("invalid csrf token")
20 )
21
22 const (
23         sessionExp = 365 * 24 * time.Hour
24 )
25
26 type respType int
27
28 const (
29         HTML respType = iota
30         JSON
31 )
32
33 type authType int
34
35 const (
36         NOAUTH authType = iota
37         SESSION
38         CSRF
39 )
40
41 type client struct {
42         *mastodon.Client
43         http.ResponseWriter
44         Req       *http.Request
45         CSRFToken string
46         Session   model.Session
47 }
48
49 func (c *client) url() string {
50         return c.Req.URL.RequestURI()
51 }
52
53 func setSessionCookie(w http.ResponseWriter, sid string, exp time.Duration) {
54         http.SetCookie(w, &http.Cookie{
55                 Name:    "session_id",
56                 Value:   sid,
57                 Expires: time.Now().Add(exp),
58         })
59 }
60
61 func writeJson(c *client, data interface{}) error {
62         return json.NewEncoder(c).Encode(map[string]interface{}{
63                 "data": data,
64         })
65 }
66
67 func redirect(c *client, url string) {
68         c.Header().Add("Location", url)
69         c.WriteHeader(http.StatusFound)
70 }
71
72 func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
73         r := mux.NewRouter()
74
75         writeError := func(c *client, err error, t respType) {
76                 switch t {
77                 case HTML:
78                         c.WriteHeader(http.StatusInternalServerError)
79                         s.ErrorPage(c, err)
80                 case JSON:
81                         c.WriteHeader(http.StatusInternalServerError)
82                         json.NewEncoder(c).Encode(map[string]string{
83                                 "error": err.Error(),
84                         })
85                 }
86         }
87
88         authenticate := func(c *client, t authType) error {
89                 if t >= SESSION {
90                         cookie, err := c.Req.Cookie("session_id")
91                         if err != nil || len(cookie.Value) < 1 {
92                                 return errInvalidSession
93                         }
94                         c.Session, err = s.sessionRepo.Get(cookie.Value)
95                         if err != nil {
96                                 return errInvalidSession
97                         }
98                         app, err := s.appRepo.Get(c.Session.InstanceDomain)
99                         if err != nil {
100                                 return err
101                         }
102                         c.Client = mastodon.NewClient(&mastodon.Config{
103                                 Server:       app.InstanceURL,
104                                 ClientID:     app.ClientID,
105                                 ClientSecret: app.ClientSecret,
106                                 AccessToken:  c.Session.AccessToken,
107                         })
108                 }
109                 if t >= CSRF {
110                         c.CSRFToken = c.Req.FormValue("csrf_token")
111                         if len(c.CSRFToken) < 1 || c.CSRFToken != c.Session.CSRFToken {
112                                 return errInvalidCSRFToken
113                         }
114                 }
115                 return nil
116         }
117
118         handle := func(f func(c *client) error, at authType, rt respType) http.HandlerFunc {
119                 return func(w http.ResponseWriter, req *http.Request) {
120                         var err error
121                         c := &client{Req: req, ResponseWriter: w}
122
123                         defer func(begin time.Time) {
124                                 logger.Printf("path=%s, err=%v, took=%v\n",
125                                         req.URL.Path, err, time.Since(begin))
126                         }(time.Now())
127
128                         var ct string
129                         switch rt {
130                         case HTML:
131                                 ct = "text/html; charset=utf-8"
132                         case JSON:
133                                 ct = "application/json"
134                         }
135                         c.Header().Add("Content-Type", ct)
136
137                         err = authenticate(c, at)
138                         if err != nil {
139                                 writeError(c, err, rt)
140                                 return
141                         }
142
143                         err = f(c)
144                         if err != nil {
145                                 writeError(c, err, rt)
146                                 return
147                         }
148                 }
149         }
150
151         rootPage := handle(func(c *client) error {
152                 sid, _ := c.Req.Cookie("session_id")
153                 if sid == nil || len(sid.Value) < 0 {
154                         redirect(c, "/signin")
155                         return nil
156                 }
157                 session, err := s.sessionRepo.Get(sid.Value)
158                 if err != nil {
159                         if err == errInvalidSession {
160                                 redirect(c, "/signin")
161                                 return nil
162                         }
163                         return err
164                 }
165                 if len(session.AccessToken) < 1 {
166                         redirect(c, "/signin")
167                         return nil
168                 }
169                 return s.RootPage(c)
170         }, NOAUTH, HTML)
171
172         navPage := handle(func(c *client) error {
173                 return s.NavPage(c)
174         }, SESSION, HTML)
175
176         signinPage := handle(func(c *client) error {
177                 instance, ok := s.SingleInstance()
178                 if !ok {
179                         return s.SigninPage(c)
180                 }
181                 url, sid, err := s.NewSession(instance)
182                 if err != nil {
183                         return err
184                 }
185                 setSessionCookie(c, sid, sessionExp)
186                 redirect(c, url)
187                 return nil
188         }, NOAUTH, HTML)
189
190         timelinePage := handle(func(c *client) error {
191                 tType, _ := mux.Vars(c.Req)["type"]
192                 q := c.Req.URL.Query()
193                 maxID := q.Get("max_id")
194                 minID := q.Get("min_id")
195                 return s.TimelinePage(c, tType, maxID, minID)
196                 return nil
197         }, SESSION, HTML)
198
199         defaultTimelinePage := handle(func(c *client) error {
200                 redirect(c, "/timeline/home")
201                 return nil
202         }, SESSION, HTML)
203
204         threadPage := handle(func(c *client) error {
205                 id, _ := mux.Vars(c.Req)["id"]
206                 q := c.Req.URL.Query()
207                 reply := q.Get("reply")
208                 return s.ThreadPage(c, id, len(reply) > 1)
209         }, SESSION, HTML)
210
211         quickReplyPage := handle(func(c *client) error {
212                 id, _ := mux.Vars(c.Req)["id"]
213                 return s.QuickReplyPage(c, id)
214         }, SESSION, HTML)
215
216         likedByPage := handle(func(c *client) error {
217                 id, _ := mux.Vars(c.Req)["id"]
218                 return s.LikedByPage(c, id)
219         }, SESSION, HTML)
220
221         retweetedByPage := handle(func(c *client) error {
222                 id, _ := mux.Vars(c.Req)["id"]
223                 return s.RetweetedByPage(c, id)
224         }, SESSION, HTML)
225
226         notificationsPage := handle(func(c *client) error {
227                 q := c.Req.URL.Query()
228                 maxID := q.Get("max_id")
229                 minID := q.Get("min_id")
230                 return s.NotificationPage(c, maxID, minID)
231         }, SESSION, HTML)
232
233         userPage := handle(func(c *client) error {
234                 id, _ := mux.Vars(c.Req)["id"]
235                 pageType, _ := mux.Vars(c.Req)["type"]
236                 q := c.Req.URL.Query()
237                 maxID := q.Get("max_id")
238                 minID := q.Get("min_id")
239                 return s.UserPage(c, id, pageType, maxID, minID)
240         }, SESSION, HTML)
241
242         userSearchPage := handle(func(c *client) error {
243                 id, _ := mux.Vars(c.Req)["id"]
244                 q := c.Req.URL.Query()
245                 sq := q.Get("q")
246                 offset, _ := strconv.Atoi(q.Get("offset"))
247                 return s.UserSearchPage(c, id, sq, offset)
248         }, SESSION, HTML)
249
250         aboutPage := handle(func(c *client) error {
251                 return s.AboutPage(c)
252         }, SESSION, HTML)
253
254         emojisPage := handle(func(c *client) error {
255                 return s.EmojiPage(c)
256         }, SESSION, HTML)
257
258         searchPage := handle(func(c *client) error {
259                 q := c.Req.URL.Query()
260                 sq := q.Get("q")
261                 qType := q.Get("type")
262                 offset, _ := strconv.Atoi(q.Get("offset"))
263                 return s.SearchPage(c, sq, qType, offset)
264         }, SESSION, HTML)
265
266         settingsPage := handle(func(c *client) error {
267                 return s.SettingsPage(c)
268         }, SESSION, HTML)
269
270         signin := handle(func(c *client) error {
271                 instance := c.Req.FormValue("instance")
272                 url, sid, err := s.NewSession(instance)
273                 if err != nil {
274                         return err
275                 }
276                 setSessionCookie(c, sid, sessionExp)
277                 redirect(c, url)
278                 return nil
279         }, NOAUTH, HTML)
280
281         oauthCallback := handle(func(c *client) error {
282                 q := c.Req.URL.Query()
283                 token := q.Get("code")
284                 token, userID, err := s.Signin(c, token)
285                 if err != nil {
286                         return err
287                 }
288
289                 c.Session.AccessToken = token
290                 c.Session.UserID = userID
291                 err = s.sessionRepo.Add(c.Session)
292                 if err != nil {
293                         return err
294                 }
295
296                 redirect(c, "/")
297                 return nil
298         }, SESSION, HTML)
299
300         post := handle(func(c *client) error {
301                 content := c.Req.FormValue("content")
302                 replyToID := c.Req.FormValue("reply_to_id")
303                 format := c.Req.FormValue("format")
304                 visibility := c.Req.FormValue("visibility")
305                 isNSFW := c.Req.FormValue("is_nsfw") == "on"
306                 files := c.Req.MultipartForm.File["attachments"]
307                 quickReply := c.Req.FormValue("quickreply") == "true"
308
309                 id, err := s.Post(c, content, replyToID, format, visibility, isNSFW, files)
310                 if err != nil {
311                         return err
312                 }
313
314                 var location string
315                 if len(replyToID) > 0 {
316                         if quickReply {
317                                 location = "/quickreply/" + id + "#status-" + id
318                         } else {
319                                 location = "/thread/" + replyToID + "#status-" + id
320                         }
321                 } else {
322                         location = c.Req.FormValue("referrer")
323                 }
324                 redirect(c, location)
325                 return nil
326         }, CSRF, HTML)
327
328         like := handle(func(c *client) error {
329                 id, _ := mux.Vars(c.Req)["id"]
330                 rid := c.Req.FormValue("retweeted_by_id")
331                 _, err := s.Like(c, id)
332                 if err != nil {
333                         return err
334                 }
335                 if len(rid) > 0 {
336                         id = rid
337                 }
338                 redirect(c, c.Req.FormValue("referrer")+"#status-"+id)
339                 return nil
340         }, CSRF, HTML)
341
342         unlike := handle(func(c *client) error {
343                 id, _ := mux.Vars(c.Req)["id"]
344                 rid := c.Req.FormValue("retweeted_by_id")
345                 _, err := s.UnLike(c, id)
346                 if err != nil {
347                         return err
348                 }
349                 if len(rid) > 0 {
350                         id = rid
351                 }
352                 redirect(c, c.Req.FormValue("referrer")+"#status-"+id)
353                 return nil
354         }, CSRF, HTML)
355
356         retweet := handle(func(c *client) error {
357                 id, _ := mux.Vars(c.Req)["id"]
358                 rid := c.Req.FormValue("retweeted_by_id")
359                 _, err := s.Retweet(c, id)
360                 if err != nil {
361                         return err
362                 }
363                 if len(rid) > 0 {
364                         id = rid
365                 }
366                 redirect(c, c.Req.FormValue("referrer")+"#status-"+id)
367                 return nil
368         }, CSRF, HTML)
369
370         unretweet := handle(func(c *client) error {
371                 id, _ := mux.Vars(c.Req)["id"]
372                 rid := c.Req.FormValue("retweeted_by_id")
373                 _, err := s.UnRetweet(c, id)
374                 if err != nil {
375                         return err
376                 }
377                 if len(rid) > 0 {
378                         id = rid
379                 }
380                 redirect(c, c.Req.FormValue("referrer")+"#status-"+id)
381                 return nil
382         }, CSRF, HTML)
383
384         vote := handle(func(c *client) error {
385                 id, _ := mux.Vars(c.Req)["id"]
386                 statusID := c.Req.FormValue("status_id")
387                 choices, _ := c.Req.PostForm["choices"]
388                 err := s.Vote(c, id, choices)
389                 if err != nil {
390                         return err
391                 }
392                 redirect(c, c.Req.FormValue("referrer")+"#status-"+statusID)
393                 return nil
394         }, CSRF, HTML)
395
396         follow := handle(func(c *client) error {
397                 id, _ := mux.Vars(c.Req)["id"]
398                 q := c.Req.URL.Query()
399                 var reblogs *bool
400                 if r, ok := q["reblogs"]; ok && len(r) > 0 {
401                         reblogs = new(bool)
402                         *reblogs = r[0] == "true"
403                 }
404                 err := s.Follow(c, id, reblogs)
405                 if err != nil {
406                         return err
407                 }
408                 redirect(c, c.Req.FormValue("referrer"))
409                 return nil
410         }, CSRF, HTML)
411
412         unfollow := handle(func(c *client) error {
413                 id, _ := mux.Vars(c.Req)["id"]
414                 err := s.UnFollow(c, id)
415                 if err != nil {
416                         return err
417                 }
418                 redirect(c, c.Req.FormValue("referrer"))
419                 return nil
420         }, CSRF, HTML)
421
422         accept := handle(func(c *client) error {
423                 id, _ := mux.Vars(c.Req)["id"]
424                 err := s.Accept(c, id)
425                 if err != nil {
426                         return err
427                 }
428                 redirect(c, c.Req.FormValue("referrer"))
429                 return nil
430         }, CSRF, HTML)
431
432         reject := handle(func(c *client) error {
433                 id, _ := mux.Vars(c.Req)["id"]
434                 err := s.Reject(c, id)
435                 if err != nil {
436                         return err
437                 }
438                 redirect(c, c.Req.FormValue("referrer"))
439                 return nil
440         }, CSRF, HTML)
441
442         mute := handle(func(c *client) error {
443                 id, _ := mux.Vars(c.Req)["id"]
444                 err := s.Mute(c, id)
445                 if err != nil {
446                         return err
447                 }
448                 redirect(c, c.Req.FormValue("referrer"))
449                 return nil
450         }, CSRF, HTML)
451
452         unMute := handle(func(c *client) error {
453                 id, _ := mux.Vars(c.Req)["id"]
454                 err := s.UnMute(c, id)
455                 if err != nil {
456                         return err
457                 }
458                 redirect(c, c.Req.FormValue("referrer"))
459                 return nil
460         }, CSRF, HTML)
461
462         block := handle(func(c *client) error {
463                 id, _ := mux.Vars(c.Req)["id"]
464                 err := s.Block(c, id)
465                 if err != nil {
466                         return err
467                 }
468                 redirect(c, c.Req.FormValue("referrer"))
469                 return nil
470         }, CSRF, HTML)
471
472         unBlock := handle(func(c *client) error {
473                 id, _ := mux.Vars(c.Req)["id"]
474                 err := s.UnBlock(c, id)
475                 if err != nil {
476                         return err
477                 }
478                 redirect(c, c.Req.FormValue("referrer"))
479                 return nil
480         }, CSRF, HTML)
481
482         subscribe := handle(func(c *client) error {
483                 id, _ := mux.Vars(c.Req)["id"]
484                 err := s.Subscribe(c, id)
485                 if err != nil {
486                         return err
487                 }
488                 redirect(c, c.Req.FormValue("referrer"))
489                 return nil
490         }, CSRF, HTML)
491
492         unSubscribe := handle(func(c *client) error {
493                 id, _ := mux.Vars(c.Req)["id"]
494                 err := s.UnSubscribe(c, id)
495                 if err != nil {
496                         return err
497                 }
498                 redirect(c, c.Req.FormValue("referrer"))
499                 return nil
500         }, CSRF, HTML)
501
502         settings := handle(func(c *client) error {
503                 visibility := c.Req.FormValue("visibility")
504                 format := c.Req.FormValue("format")
505                 copyScope := c.Req.FormValue("copy_scope") == "true"
506                 threadInNewTab := c.Req.FormValue("thread_in_new_tab") == "true"
507                 hideAttachments := c.Req.FormValue("hide_attachments") == "true"
508                 maskNSFW := c.Req.FormValue("mask_nsfw") == "true"
509                 ni, _ := strconv.Atoi(c.Req.FormValue("notification_interval"))
510                 fluorideMode := c.Req.FormValue("fluoride_mode") == "true"
511                 darkMode := c.Req.FormValue("dark_mode") == "true"
512                 antiDopamineMode := c.Req.FormValue("anti_dopamine_mode") == "true"
513                 customCSS := c.Req.FormValue("custom_css")
514
515                 settings := &model.Settings{
516                         DefaultVisibility:    visibility,
517                         DefaultFormat:        format,
518                         CopyScope:            copyScope,
519                         ThreadInNewTab:       threadInNewTab,
520                         HideAttachments:      hideAttachments,
521                         MaskNSFW:             maskNSFW,
522                         NotificationInterval: ni,
523                         FluorideMode:         fluorideMode,
524                         DarkMode:             darkMode,
525                         AntiDopamineMode:     antiDopamineMode,
526                         CustomCSS:            customCSS,
527                 }
528
529                 err := s.SaveSettings(c, settings)
530                 if err != nil {
531                         return err
532                 }
533                 redirect(c, "/")
534                 return nil
535         }, CSRF, HTML)
536
537         muteConversation := handle(func(c *client) error {
538                 id, _ := mux.Vars(c.Req)["id"]
539                 err := s.MuteConversation(c, id)
540                 if err != nil {
541                         return err
542                 }
543                 redirect(c, c.Req.FormValue("referrer"))
544                 return nil
545         }, CSRF, HTML)
546
547         unMuteConversation := handle(func(c *client) error {
548                 id, _ := mux.Vars(c.Req)["id"]
549                 err := s.UnMuteConversation(c, id)
550                 if err != nil {
551                         return err
552                 }
553                 redirect(c, c.Req.FormValue("referrer"))
554                 return nil
555         }, CSRF, HTML)
556
557         delete := handle(func(c *client) error {
558                 id, _ := mux.Vars(c.Req)["id"]
559                 err := s.Delete(c, id)
560                 if err != nil {
561                         return err
562                 }
563                 redirect(c, c.Req.FormValue("referrer"))
564                 return nil
565         }, CSRF, HTML)
566
567         readNotifications := handle(func(c *client) error {
568                 q := c.Req.URL.Query()
569                 maxID := q.Get("max_id")
570                 err := s.ReadNotifications(c, maxID)
571                 if err != nil {
572                         return err
573                 }
574                 redirect(c, c.Req.FormValue("referrer"))
575                 return nil
576         }, CSRF, HTML)
577
578         bookmark := handle(func(c *client) error {
579                 id, _ := mux.Vars(c.Req)["id"]
580                 rid := c.Req.FormValue("retweeted_by_id")
581                 err := s.Bookmark(c, id)
582                 if err != nil {
583                         return err
584                 }
585                 if len(rid) > 0 {
586                         id = rid
587                 }
588                 redirect(c, c.Req.FormValue("referrer")+"#status-"+id)
589                 return nil
590         }, CSRF, HTML)
591
592         unBookmark := handle(func(c *client) error {
593                 id, _ := mux.Vars(c.Req)["id"]
594                 rid := c.Req.FormValue("retweeted_by_id")
595                 err := s.UnBookmark(c, id)
596                 if err != nil {
597                         return err
598                 }
599                 if len(rid) > 0 {
600                         id = rid
601                 }
602                 redirect(c, c.Req.FormValue("referrer")+"#status-"+id)
603                 return nil
604         }, CSRF, HTML)
605
606         signout := handle(func(c *client) error {
607                 s.Signout(c)
608                 setSessionCookie(c, "", 0)
609                 redirect(c, "/")
610                 return nil
611         }, CSRF, HTML)
612
613         fLike := handle(func(c *client) error {
614                 id, _ := mux.Vars(c.Req)["id"]
615                 count, err := s.Like(c, id)
616                 if err != nil {
617                         return err
618                 }
619                 return writeJson(c, count)
620         }, CSRF, JSON)
621
622         fUnlike := handle(func(c *client) error {
623                 id, _ := mux.Vars(c.Req)["id"]
624                 count, err := s.UnLike(c, id)
625                 if err != nil {
626                         return err
627                 }
628                 return writeJson(c, count)
629         }, CSRF, JSON)
630
631         fRetweet := handle(func(c *client) error {
632                 id, _ := mux.Vars(c.Req)["id"]
633                 count, err := s.Retweet(c, id)
634                 if err != nil {
635                         return err
636                 }
637                 return writeJson(c, count)
638         }, CSRF, JSON)
639
640         fUnretweet := handle(func(c *client) error {
641                 id, _ := mux.Vars(c.Req)["id"]
642                 count, err := s.UnRetweet(c, id)
643                 if err != nil {
644                         return err
645                 }
646                 return writeJson(c, count)
647         }, CSRF, JSON)
648
649         r.HandleFunc("/", rootPage).Methods(http.MethodGet)
650         r.HandleFunc("/nav", navPage).Methods(http.MethodGet)
651         r.HandleFunc("/signin", signinPage).Methods(http.MethodGet)
652         r.HandleFunc("/timeline/{type}", timelinePage).Methods(http.MethodGet)
653         r.HandleFunc("/timeline", defaultTimelinePage).Methods(http.MethodGet)
654         r.HandleFunc("/thread/{id}", threadPage).Methods(http.MethodGet)
655         r.HandleFunc("/quickreply/{id}", quickReplyPage).Methods(http.MethodGet)
656         r.HandleFunc("/likedby/{id}", likedByPage).Methods(http.MethodGet)
657         r.HandleFunc("/retweetedby/{id}", retweetedByPage).Methods(http.MethodGet)
658         r.HandleFunc("/notifications", notificationsPage).Methods(http.MethodGet)
659         r.HandleFunc("/user/{id}", userPage).Methods(http.MethodGet)
660         r.HandleFunc("/user/{id}/{type}", userPage).Methods(http.MethodGet)
661         r.HandleFunc("/usersearch/{id}", userSearchPage).Methods(http.MethodGet)
662         r.HandleFunc("/about", aboutPage).Methods(http.MethodGet)
663         r.HandleFunc("/emojis", emojisPage).Methods(http.MethodGet)
664         r.HandleFunc("/search", searchPage).Methods(http.MethodGet)
665         r.HandleFunc("/settings", settingsPage).Methods(http.MethodGet)
666         r.HandleFunc("/signin", signin).Methods(http.MethodPost)
667         r.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet)
668         r.HandleFunc("/post", post).Methods(http.MethodPost)
669         r.HandleFunc("/like/{id}", like).Methods(http.MethodPost)
670         r.HandleFunc("/unlike/{id}", unlike).Methods(http.MethodPost)
671         r.HandleFunc("/retweet/{id}", retweet).Methods(http.MethodPost)
672         r.HandleFunc("/unretweet/{id}", unretweet).Methods(http.MethodPost)
673         r.HandleFunc("/vote/{id}", vote).Methods(http.MethodPost)
674         r.HandleFunc("/follow/{id}", follow).Methods(http.MethodPost)
675         r.HandleFunc("/unfollow/{id}", unfollow).Methods(http.MethodPost)
676         r.HandleFunc("/accept/{id}", accept).Methods(http.MethodPost)
677         r.HandleFunc("/reject/{id}", reject).Methods(http.MethodPost)
678         r.HandleFunc("/mute/{id}", mute).Methods(http.MethodPost)
679         r.HandleFunc("/unmute/{id}", unMute).Methods(http.MethodPost)
680         r.HandleFunc("/block/{id}", block).Methods(http.MethodPost)
681         r.HandleFunc("/unblock/{id}", unBlock).Methods(http.MethodPost)
682         r.HandleFunc("/subscribe/{id}", subscribe).Methods(http.MethodPost)
683         r.HandleFunc("/unsubscribe/{id}", unSubscribe).Methods(http.MethodPost)
684         r.HandleFunc("/settings", settings).Methods(http.MethodPost)
685         r.HandleFunc("/muteconv/{id}", muteConversation).Methods(http.MethodPost)
686         r.HandleFunc("/unmuteconv/{id}", unMuteConversation).Methods(http.MethodPost)
687         r.HandleFunc("/delete/{id}", delete).Methods(http.MethodPost)
688         r.HandleFunc("/notifications/read", readNotifications).Methods(http.MethodPost)
689         r.HandleFunc("/bookmark/{id}", bookmark).Methods(http.MethodPost)
690         r.HandleFunc("/unbookmark/{id}", unBookmark).Methods(http.MethodPost)
691         r.HandleFunc("/signout", signout).Methods(http.MethodPost)
692         r.HandleFunc("/fluoride/like/{id}", fLike).Methods(http.MethodPost)
693         r.HandleFunc("/fluoride/unlike/{id}", fUnlike).Methods(http.MethodPost)
694         r.HandleFunc("/fluoride/retweet/{id}", fRetweet).Methods(http.MethodPost)
695         r.HandleFunc("/fluoride/unretweet/{id}", fUnretweet).Methods(http.MethodPost)
696         r.PathPrefix("/static").Handler(http.StripPrefix("/static",
697                 http.FileServer(http.Dir(staticDir))))
698
699         return r
700 }