transport.go 17 KB

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