service.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "html/template"
  7. "mime/multipart"
  8. "net/url"
  9. "strings"
  10. "bloat/mastodon"
  11. "bloat/model"
  12. "bloat/renderer"
  13. "bloat/util"
  14. )
  15. var (
  16. ctx = context.Background()
  17. errInvalidArgument = errors.New("invalid argument")
  18. )
  19. type service struct {
  20. clientName string
  21. clientScope string
  22. clientWebsite string
  23. customCSS string
  24. postFormats []model.PostFormat
  25. renderer renderer.Renderer
  26. sessionRepo model.SessionRepo
  27. appRepo model.AppRepo
  28. singleInstance string
  29. }
  30. func NewService(clientName string,
  31. clientScope string,
  32. clientWebsite string,
  33. customCSS string,
  34. postFormats []model.PostFormat,
  35. renderer renderer.Renderer,
  36. sessionRepo model.SessionRepo,
  37. appRepo model.AppRepo,
  38. singleInstance string,
  39. ) *service {
  40. return &service{
  41. clientName: clientName,
  42. clientScope: clientScope,
  43. clientWebsite: clientWebsite,
  44. customCSS: customCSS,
  45. postFormats: postFormats,
  46. renderer: renderer,
  47. sessionRepo: sessionRepo,
  48. appRepo: appRepo,
  49. singleInstance: singleInstance,
  50. }
  51. }
  52. func getRendererContext(c *client) *renderer.Context {
  53. var settings model.Settings
  54. var session model.Session
  55. if c != nil {
  56. settings = c.Session.Settings
  57. session = c.Session
  58. } else {
  59. settings = *model.NewSettings()
  60. }
  61. return &renderer.Context{
  62. HideAttachments: settings.HideAttachments,
  63. MaskNSFW: settings.MaskNSFW,
  64. ThreadInNewTab: settings.ThreadInNewTab,
  65. FluorideMode: settings.FluorideMode,
  66. DarkMode: settings.DarkMode,
  67. CSRFToken: session.CSRFToken,
  68. UserID: session.UserID,
  69. AntiDopamineMode: settings.AntiDopamineMode,
  70. }
  71. }
  72. func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{},
  73. val string, number int) {
  74. if key == nil {
  75. return
  76. }
  77. keyStr, ok := key.(string)
  78. if !ok {
  79. return
  80. }
  81. _, ok = m[keyStr]
  82. if !ok {
  83. m[keyStr] = []mastodon.ReplyInfo{}
  84. }
  85. m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
  86. }
  87. func (s *service) getCommonData(c *client, title string) (data *renderer.CommonData) {
  88. data = &renderer.CommonData{
  89. Title: title + " - " + s.clientName,
  90. CustomCSS: s.customCSS,
  91. }
  92. if c != nil && c.Session.IsLoggedIn() {
  93. data.CSRFToken = c.Session.CSRFToken
  94. }
  95. return
  96. }
  97. func (s *service) ErrorPage(c *client, err error) {
  98. var errStr string
  99. if err != nil {
  100. errStr = err.Error()
  101. }
  102. commonData := s.getCommonData(nil, "error")
  103. data := &renderer.ErrorData{
  104. CommonData: commonData,
  105. Error: errStr,
  106. }
  107. rCtx := getRendererContext(c)
  108. s.renderer.Render(rCtx, c, renderer.ErrorPage, data)
  109. }
  110. func (s *service) SigninPage(c *client) (err error) {
  111. commonData := s.getCommonData(nil, "signin")
  112. data := &renderer.SigninData{
  113. CommonData: commonData,
  114. }
  115. rCtx := getRendererContext(nil)
  116. return s.renderer.Render(rCtx, c, renderer.SigninPage, data)
  117. }
  118. func (s *service) RootPage(c *client) (err error) {
  119. data := &renderer.RootData{
  120. Title: s.clientName,
  121. }
  122. rCtx := getRendererContext(c)
  123. return s.renderer.Render(rCtx, c, renderer.RootPage, data)
  124. }
  125. func (s *service) NavPage(c *client) (err error) {
  126. u, err := c.GetAccountCurrentUser(ctx)
  127. if err != nil {
  128. return
  129. }
  130. postContext := model.PostContext{
  131. DefaultVisibility: c.Session.Settings.DefaultVisibility,
  132. DefaultFormat: c.Session.Settings.DefaultFormat,
  133. Formats: s.postFormats,
  134. }
  135. commonData := s.getCommonData(c, "nav")
  136. commonData.Target = "main"
  137. data := &renderer.NavData{
  138. User: u,
  139. CommonData: commonData,
  140. PostContext: postContext,
  141. }
  142. rCtx := getRendererContext(c)
  143. return s.renderer.Render(rCtx, c, renderer.NavPage, data)
  144. }
  145. func (s *service) TimelinePage(c *client, tType string, maxID string,
  146. minID string, instance string) (err error) {
  147. var nextLink, prevLink, title string
  148. var statuses []*mastodon.Status
  149. var pg = mastodon.Pagination{
  150. MaxID: maxID,
  151. MinID: minID,
  152. Limit: 20,
  153. }
  154. switch tType {
  155. default:
  156. return errInvalidArgument
  157. case "home":
  158. statuses, err = c.GetTimelineHome(ctx, &pg)
  159. title = "Timeline"
  160. case "direct":
  161. statuses, err = c.GetTimelineDirect(ctx, &pg)
  162. title = "Direct Timeline"
  163. case "local":
  164. statuses, err = c.GetTimelinePublic(ctx, true, &pg)
  165. title = "Local Timeline"
  166. case "twkn":
  167. statuses, err = c.GetTimelinePublic(ctx, false, &pg)
  168. title = "The Whole Known Network"
  169. case "remote":
  170. if len(instance) < 1 {
  171. return errInvalidArgument
  172. }
  173. var instanceURL string
  174. if strings.HasPrefix(instance, "https://") {
  175. instanceURL = instance
  176. instance = strings.TrimPrefix(instance, "https://")
  177. } else {
  178. instanceURL = "https://" + instance
  179. }
  180. c.Client = mastodon.NewClient(&mastodon.Config{
  181. Server: instanceURL,
  182. })
  183. statuses, err = c.GetTimelinePublic(ctx, true, &pg)
  184. title = "Remote Timeline of " + instance
  185. }
  186. if err != nil {
  187. return err
  188. }
  189. for i := range statuses {
  190. if statuses[i].Reblog != nil {
  191. statuses[i].Reblog.RetweetedByID = statuses[i].ID
  192. }
  193. }
  194. if (len(maxID) > 0 || len(minID) > 0) && len(statuses) > 0 {
  195. q := make(url.Values)
  196. q["min_id"] = []string{statuses[0].ID}
  197. if tType == "remote" {
  198. q["instance"] = []string{instance}
  199. }
  200. prevLink = fmt.Sprintf("/timeline/%s?%s", tType, q.Encode())
  201. }
  202. if len(pg.MaxID) > 0 && len(statuses) == 20 {
  203. q := make(url.Values)
  204. q["max_id"] = []string{pg.MaxID}
  205. if tType == "remote" {
  206. q["instance"] = []string{instance}
  207. }
  208. nextLink = fmt.Sprintf("/timeline/%s?%s", tType, q.Encode())
  209. }
  210. commonData := s.getCommonData(c, tType+" timeline ")
  211. data := &renderer.TimelineData{
  212. Title: title,
  213. Statuses: statuses,
  214. NextLink: nextLink,
  215. PrevLink: prevLink,
  216. CommonData: commonData,
  217. }
  218. rCtx := getRendererContext(c)
  219. return s.renderer.Render(rCtx, c, renderer.TimelinePage, data)
  220. }
  221. func (s *service) ThreadPage(c *client, id string, reply bool) (err error) {
  222. var postContext model.PostContext
  223. status, err := c.GetStatus(ctx, id)
  224. if err != nil {
  225. return
  226. }
  227. if reply {
  228. var content string
  229. var visibility string
  230. if c.Session.UserID != status.Account.ID {
  231. content += "@" + status.Account.Acct + " "
  232. }
  233. for i := range status.Mentions {
  234. if status.Mentions[i].ID != c.Session.UserID &&
  235. status.Mentions[i].ID != status.Account.ID {
  236. content += "@" + status.Mentions[i].Acct + " "
  237. }
  238. }
  239. isDirect := status.Visibility == "direct"
  240. if isDirect || c.Session.Settings.CopyScope {
  241. visibility = status.Visibility
  242. } else {
  243. visibility = c.Session.Settings.DefaultVisibility
  244. }
  245. postContext = model.PostContext{
  246. DefaultVisibility: visibility,
  247. DefaultFormat: c.Session.Settings.DefaultFormat,
  248. Formats: s.postFormats,
  249. ReplyContext: &model.ReplyContext{
  250. InReplyToID: id,
  251. InReplyToName: status.Account.Acct,
  252. ReplyContent: content,
  253. ForceVisibility: isDirect,
  254. },
  255. }
  256. }
  257. context, err := c.GetStatusContext(ctx, id)
  258. if err != nil {
  259. return
  260. }
  261. statuses := append(append(context.Ancestors, status), context.Descendants...)
  262. replies := make(map[string][]mastodon.ReplyInfo)
  263. idNumbers := make(map[string]int)
  264. for i := range statuses {
  265. statuses[i].ShowReplies = true
  266. statuses[i].IDNumbers = idNumbers
  267. idNumbers[statuses[i].ID] = i + 1
  268. statuses[i].IDReplies = replies
  269. addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)
  270. }
  271. commonData := s.getCommonData(c, "post by "+status.Account.DisplayName)
  272. data := &renderer.ThreadData{
  273. Statuses: statuses,
  274. PostContext: postContext,
  275. ReplyMap: replies,
  276. CommonData: commonData,
  277. }
  278. rCtx := getRendererContext(c)
  279. return s.renderer.Render(rCtx, c, renderer.ThreadPage, data)
  280. }
  281. func (s *service) LikedByPage(c *client, id string) (err error) {
  282. likers, err := c.GetFavouritedBy(ctx, id, nil)
  283. if err != nil {
  284. return
  285. }
  286. commonData := s.getCommonData(c, "likes")
  287. data := &renderer.LikedByData{
  288. CommonData: commonData,
  289. Users: likers,
  290. }
  291. rCtx := getRendererContext(c)
  292. return s.renderer.Render(rCtx, c, renderer.LikedByPage, data)
  293. }
  294. func (s *service) RetweetedByPage(c *client, id string) (err error) {
  295. retweeters, err := c.GetRebloggedBy(ctx, id, nil)
  296. if err != nil {
  297. return
  298. }
  299. commonData := s.getCommonData(c, "retweets")
  300. data := &renderer.RetweetedByData{
  301. CommonData: commonData,
  302. Users: retweeters,
  303. }
  304. rCtx := getRendererContext(c)
  305. return s.renderer.Render(rCtx, c, renderer.RetweetedByPage, data)
  306. }
  307. func (s *service) NotificationPage(c *client, maxID string,
  308. minID string) (err error) {
  309. var nextLink string
  310. var unreadCount int
  311. var readID string
  312. var excludes []string
  313. var pg = mastodon.Pagination{
  314. MaxID: maxID,
  315. MinID: minID,
  316. Limit: 20,
  317. }
  318. if c.Session.Settings.AntiDopamineMode {
  319. excludes = []string{"follow", "favourite", "reblog"}
  320. }
  321. notifications, err := c.GetNotifications(ctx, &pg, excludes)
  322. if err != nil {
  323. return
  324. }
  325. for i := range notifications {
  326. if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
  327. unreadCount++
  328. }
  329. }
  330. if unreadCount > 0 {
  331. readID = notifications[0].ID
  332. }
  333. if len(notifications) == 20 && len(pg.MaxID) > 0 {
  334. nextLink = "/notifications?max_id=" + pg.MaxID
  335. }
  336. commonData := s.getCommonData(c, "notifications")
  337. commonData.RefreshInterval = c.Session.Settings.NotificationInterval
  338. commonData.Target = "main"
  339. commonData.Count = unreadCount
  340. data := &renderer.NotificationData{
  341. Notifications: notifications,
  342. UnreadCount: unreadCount,
  343. ReadID: readID,
  344. NextLink: nextLink,
  345. CommonData: commonData,
  346. }
  347. rCtx := getRendererContext(c)
  348. return s.renderer.Render(rCtx, c, renderer.NotificationPage, data)
  349. }
  350. func (s *service) UserPage(c *client, id string, pageType string,
  351. maxID string, minID string) (err error) {
  352. var nextLink string
  353. var statuses []*mastodon.Status
  354. var users []*mastodon.Account
  355. var pg = mastodon.Pagination{
  356. MaxID: maxID,
  357. MinID: minID,
  358. Limit: 20,
  359. }
  360. user, err := c.GetAccount(ctx, id)
  361. if err != nil {
  362. return
  363. }
  364. isCurrent := c.Session.UserID == user.ID
  365. switch pageType {
  366. case "":
  367. statuses, err = c.GetAccountStatuses(ctx, id, false, &pg)
  368. if err != nil {
  369. return
  370. }
  371. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  372. nextLink = fmt.Sprintf("/user/%s?max_id=%s", id,
  373. pg.MaxID)
  374. }
  375. case "following":
  376. users, err = c.GetAccountFollowing(ctx, id, &pg)
  377. if err != nil {
  378. return
  379. }
  380. if len(users) == 20 && len(pg.MaxID) > 0 {
  381. nextLink = fmt.Sprintf("/user/%s/following?max_id=%s",
  382. id, pg.MaxID)
  383. }
  384. case "followers":
  385. users, err = c.GetAccountFollowers(ctx, id, &pg)
  386. if err != nil {
  387. return
  388. }
  389. if len(users) == 20 && len(pg.MaxID) > 0 {
  390. nextLink = fmt.Sprintf("/user/%s/followers?max_id=%s",
  391. id, pg.MaxID)
  392. }
  393. case "media":
  394. statuses, err = c.GetAccountStatuses(ctx, id, true, &pg)
  395. if err != nil {
  396. return
  397. }
  398. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  399. nextLink = fmt.Sprintf("/user/%s/media?max_id=%s",
  400. id, pg.MaxID)
  401. }
  402. case "bookmarks":
  403. if !isCurrent {
  404. return errInvalidArgument
  405. }
  406. statuses, err = c.GetBookmarks(ctx, &pg)
  407. if err != nil {
  408. return
  409. }
  410. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  411. nextLink = fmt.Sprintf("/user/%s/bookmarks?max_id=%s",
  412. id, pg.MaxID)
  413. }
  414. case "mutes":
  415. if !isCurrent {
  416. return errInvalidArgument
  417. }
  418. users, err = c.GetMutes(ctx, &pg)
  419. if err != nil {
  420. return
  421. }
  422. if len(users) == 20 && len(pg.MaxID) > 0 {
  423. nextLink = fmt.Sprintf("/user/%s/mutes?max_id=%s",
  424. id, pg.MaxID)
  425. }
  426. case "blocks":
  427. if !isCurrent {
  428. return errInvalidArgument
  429. }
  430. users, err = c.GetBlocks(ctx, &pg)
  431. if err != nil {
  432. return
  433. }
  434. if len(users) == 20 && len(pg.MaxID) > 0 {
  435. nextLink = fmt.Sprintf("/user/%s/blocks?max_id=%s",
  436. id, pg.MaxID)
  437. }
  438. case "likes":
  439. if !isCurrent {
  440. return errInvalidArgument
  441. }
  442. statuses, err = c.GetFavourites(ctx, &pg)
  443. if err != nil {
  444. return
  445. }
  446. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  447. nextLink = fmt.Sprintf("/user/%s/likes?max_id=%s",
  448. id, pg.MaxID)
  449. }
  450. default:
  451. return errInvalidArgument
  452. }
  453. for i := range statuses {
  454. if statuses[i].Reblog != nil {
  455. statuses[i].Reblog.RetweetedByID = statuses[i].ID
  456. }
  457. }
  458. commonData := s.getCommonData(c, user.DisplayName)
  459. data := &renderer.UserData{
  460. User: user,
  461. IsCurrent: isCurrent,
  462. Type: pageType,
  463. Users: users,
  464. Statuses: statuses,
  465. NextLink: nextLink,
  466. CommonData: commonData,
  467. }
  468. rCtx := getRendererContext(c)
  469. return s.renderer.Render(rCtx, c, renderer.UserPage, data)
  470. }
  471. func (s *service) UserSearchPage(c *client,
  472. id string, q string, offset int) (err error) {
  473. var nextLink string
  474. var title = "search"
  475. user, err := c.GetAccount(ctx, id)
  476. if err != nil {
  477. return
  478. }
  479. var results *mastodon.Results
  480. if len(q) > 0 {
  481. results, err = c.Search(ctx, q, "statuses", 20, true, offset, id)
  482. if err != nil {
  483. return err
  484. }
  485. } else {
  486. results = &mastodon.Results{}
  487. }
  488. if len(results.Statuses) == 20 {
  489. offset += 20
  490. nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id,
  491. url.QueryEscape(q), offset)
  492. }
  493. qq := template.HTMLEscapeString(q)
  494. if len(q) > 0 {
  495. title += " \"" + qq + "\""
  496. }
  497. commonData := s.getCommonData(c, title)
  498. data := &renderer.UserSearchData{
  499. CommonData: commonData,
  500. User: user,
  501. Q: qq,
  502. Statuses: results.Statuses,
  503. NextLink: nextLink,
  504. }
  505. rCtx := getRendererContext(c)
  506. return s.renderer.Render(rCtx, c, renderer.UserSearchPage, data)
  507. }
  508. func (s *service) AboutPage(c *client) (err error) {
  509. commonData := s.getCommonData(c, "about")
  510. data := &renderer.AboutData{
  511. CommonData: commonData,
  512. }
  513. rCtx := getRendererContext(c)
  514. return s.renderer.Render(rCtx, c, renderer.AboutPage, data)
  515. }
  516. func (s *service) EmojiPage(c *client) (err error) {
  517. emojis, err := c.GetInstanceEmojis(ctx)
  518. if err != nil {
  519. return
  520. }
  521. commonData := s.getCommonData(c, "emojis")
  522. data := &renderer.EmojiData{
  523. Emojis: emojis,
  524. CommonData: commonData,
  525. }
  526. rCtx := getRendererContext(c)
  527. return s.renderer.Render(rCtx, c, renderer.EmojiPage, data)
  528. }
  529. func (s *service) SearchPage(c *client,
  530. q string, qType string, offset int) (err error) {
  531. var nextLink string
  532. var title = "search"
  533. var results *mastodon.Results
  534. if len(q) > 0 {
  535. results, err = c.Search(ctx, q, qType, 20, true, offset, "")
  536. if err != nil {
  537. return err
  538. }
  539. } else {
  540. results = &mastodon.Results{}
  541. }
  542. if (qType == "accounts" && len(results.Accounts) == 20) ||
  543. (qType == "statuses" && len(results.Statuses) == 20) {
  544. offset += 20
  545. nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d",
  546. url.QueryEscape(q), qType, offset)
  547. }
  548. qq := template.HTMLEscapeString(q)
  549. if len(q) > 0 {
  550. title += " \"" + qq + "\""
  551. }
  552. commonData := s.getCommonData(c, title)
  553. data := &renderer.SearchData{
  554. CommonData: commonData,
  555. Q: qq,
  556. Type: qType,
  557. Users: results.Accounts,
  558. Statuses: results.Statuses,
  559. NextLink: nextLink,
  560. }
  561. rCtx := getRendererContext(c)
  562. return s.renderer.Render(rCtx, c, renderer.SearchPage, data)
  563. }
  564. func (s *service) SettingsPage(c *client) (err error) {
  565. commonData := s.getCommonData(c, "settings")
  566. data := &renderer.SettingsData{
  567. CommonData: commonData,
  568. Settings: &c.Session.Settings,
  569. PostFormats: s.postFormats,
  570. }
  571. rCtx := getRendererContext(c)
  572. return s.renderer.Render(rCtx, c, renderer.SettingsPage, data)
  573. }
  574. func (s *service) SingleInstance() (instance string, ok bool) {
  575. if len(s.singleInstance) > 0 {
  576. instance = s.singleInstance
  577. ok = true
  578. }
  579. return
  580. }
  581. func (s *service) NewSession(instance string) (rurl string, sid string, err error) {
  582. var instanceURL string
  583. if strings.HasPrefix(instance, "https://") {
  584. instanceURL = instance
  585. instance = strings.TrimPrefix(instance, "https://")
  586. } else {
  587. instanceURL = "https://" + instance
  588. }
  589. sid, err = util.NewSessionID()
  590. if err != nil {
  591. return
  592. }
  593. csrfToken, err := util.NewCSRFToken()
  594. if err != nil {
  595. return
  596. }
  597. session := model.Session{
  598. ID: sid,
  599. InstanceDomain: instance,
  600. CSRFToken: csrfToken,
  601. Settings: *model.NewSettings(),
  602. }
  603. err = s.sessionRepo.Add(session)
  604. if err != nil {
  605. return
  606. }
  607. app, err := s.appRepo.Get(instance)
  608. if err != nil {
  609. if err != model.ErrAppNotFound {
  610. return
  611. }
  612. mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{
  613. Server: instanceURL,
  614. ClientName: s.clientName,
  615. Scopes: s.clientScope,
  616. Website: s.clientWebsite,
  617. RedirectURIs: s.clientWebsite + "/oauth_callback",
  618. })
  619. if err != nil {
  620. return "", "", err
  621. }
  622. app = model.App{
  623. InstanceDomain: instance,
  624. InstanceURL: instanceURL,
  625. ClientID: mastoApp.ClientID,
  626. ClientSecret: mastoApp.ClientSecret,
  627. }
  628. err = s.appRepo.Add(app)
  629. if err != nil {
  630. return "", "", err
  631. }
  632. }
  633. u, err := url.Parse("/oauth/authorize")
  634. if err != nil {
  635. return
  636. }
  637. q := make(url.Values)
  638. q.Set("scope", "read write follow")
  639. q.Set("client_id", app.ClientID)
  640. q.Set("response_type", "code")
  641. q.Set("redirect_uri", s.clientWebsite+"/oauth_callback")
  642. u.RawQuery = q.Encode()
  643. rurl = instanceURL + u.String()
  644. return
  645. }
  646. func (s *service) Signin(c *client, code string) (token string,
  647. userID string, err error) {
  648. if len(code) < 1 {
  649. err = errInvalidArgument
  650. return
  651. }
  652. err = c.AuthenticateToken(ctx, code, s.clientWebsite+"/oauth_callback")
  653. if err != nil {
  654. return
  655. }
  656. token = c.GetAccessToken(ctx)
  657. u, err := c.GetAccountCurrentUser(ctx)
  658. if err != nil {
  659. return
  660. }
  661. userID = u.ID
  662. return
  663. }
  664. func (s *service) Signout(c *client) (err error) {
  665. s.sessionRepo.Remove(c.Session.ID)
  666. return
  667. }
  668. func (s *service) Post(c *client, content string, replyToID string,
  669. format string, visibility string, isNSFW bool,
  670. files []*multipart.FileHeader) (id string, err error) {
  671. var mediaIDs []string
  672. for _, f := range files {
  673. a, err := c.UploadMediaFromMultipartFileHeader(ctx, f)
  674. if err != nil {
  675. return "", err
  676. }
  677. mediaIDs = append(mediaIDs, a.ID)
  678. }
  679. tweet := &mastodon.Toot{
  680. Status: content,
  681. InReplyToID: replyToID,
  682. MediaIDs: mediaIDs,
  683. ContentType: format,
  684. Visibility: visibility,
  685. Sensitive: isNSFW,
  686. }
  687. st, err := c.PostStatus(ctx, tweet)
  688. if err != nil {
  689. return
  690. }
  691. return st.ID, nil
  692. }
  693. func (s *service) Like(c *client, id string) (count int64, err error) {
  694. st, err := c.Favourite(ctx, id)
  695. if err != nil {
  696. return
  697. }
  698. count = st.FavouritesCount
  699. return
  700. }
  701. func (s *service) UnLike(c *client, id string) (count int64, err error) {
  702. st, err := c.Unfavourite(ctx, id)
  703. if err != nil {
  704. return
  705. }
  706. count = st.FavouritesCount
  707. return
  708. }
  709. func (s *service) Retweet(c *client, id string) (count int64, err error) {
  710. st, err := c.Reblog(ctx, id)
  711. if err != nil {
  712. return
  713. }
  714. if st.Reblog != nil {
  715. count = st.Reblog.ReblogsCount
  716. }
  717. return
  718. }
  719. func (s *service) UnRetweet(c *client, id string) (
  720. count int64, err error) {
  721. st, err := c.Unreblog(ctx, id)
  722. if err != nil {
  723. return
  724. }
  725. count = st.ReblogsCount
  726. return
  727. }
  728. func (s *service) Vote(c *client, id string, choices []string) (err error) {
  729. _, err = c.Vote(ctx, id, choices)
  730. return
  731. }
  732. func (s *service) Follow(c *client, id string, reblogs *bool) (err error) {
  733. _, err = c.AccountFollow(ctx, id, reblogs)
  734. return
  735. }
  736. func (s *service) UnFollow(c *client, id string) (err error) {
  737. _, err = c.AccountUnfollow(ctx, id)
  738. return
  739. }
  740. func (s *service) Mute(c *client, id string) (err error) {
  741. _, err = c.AccountMute(ctx, id)
  742. return
  743. }
  744. func (s *service) UnMute(c *client, id string) (err error) {
  745. _, err = c.AccountUnmute(ctx, id)
  746. return
  747. }
  748. func (s *service) Block(c *client, id string) (err error) {
  749. _, err = c.AccountBlock(ctx, id)
  750. return
  751. }
  752. func (s *service) UnBlock(c *client, id string) (err error) {
  753. _, err = c.AccountUnblock(ctx, id)
  754. return
  755. }
  756. func (s *service) Subscribe(c *client, id string) (err error) {
  757. _, err = c.Subscribe(ctx, id)
  758. return
  759. }
  760. func (s *service) UnSubscribe(c *client, id string) (err error) {
  761. _, err = c.UnSubscribe(ctx, id)
  762. return
  763. }
  764. func (s *service) SaveSettings(c *client, settings *model.Settings) (err error) {
  765. switch settings.NotificationInterval {
  766. case 0, 30, 60, 120, 300, 600:
  767. default:
  768. return errInvalidArgument
  769. }
  770. session, err := s.sessionRepo.Get(c.Session.ID)
  771. if err != nil {
  772. return
  773. }
  774. session.Settings = *settings
  775. return s.sessionRepo.Add(session)
  776. }
  777. func (s *service) MuteConversation(c *client, id string) (err error) {
  778. _, err = c.MuteConversation(ctx, id)
  779. return
  780. }
  781. func (s *service) UnMuteConversation(c *client, id string) (err error) {
  782. _, err = c.UnmuteConversation(ctx, id)
  783. return
  784. }
  785. func (s *service) Delete(c *client, id string) (err error) {
  786. return c.DeleteStatus(ctx, id)
  787. }
  788. func (s *service) ReadNotifications(c *client, maxID string) (err error) {
  789. return c.ReadNotifications(ctx, maxID)
  790. }
  791. func (s *service) Bookmark(c *client, id string) (err error) {
  792. _, err = c.Bookmark(ctx, id)
  793. return
  794. }
  795. func (s *service) UnBookmark(c *client, id string) (err error) {
  796. _, err = c.Unbookmark(ctx, id)
  797. return
  798. }