service.go 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226
  1. package service
  2. import (
  3. "crypto/sha256"
  4. "encoding/base64"
  5. "errors"
  6. "fmt"
  7. "mime/multipart"
  8. "net/url"
  9. "strings"
  10. "bloat/mastodon"
  11. "bloat/model"
  12. "bloat/renderer"
  13. "bloat/util"
  14. "github.com/enescakir/emoji"
  15. )
  16. var (
  17. errInvalidArgument = errors.New("invalid argument")
  18. errInvalidSession = errors.New("invalid session")
  19. errInvalidCSRFToken = errors.New("invalid csrf token")
  20. )
  21. type service struct {
  22. cname string
  23. cscope string
  24. cwebsite string
  25. css string
  26. instance string
  27. postFormats []model.PostFormat
  28. renderer renderer.Renderer
  29. }
  30. func NewService(cname string, cscope string, cwebsite string,
  31. css string, instance string, postFormats []model.PostFormat,
  32. renderer renderer.Renderer) *service {
  33. return &service{
  34. cname: cname,
  35. cscope: cscope,
  36. cwebsite: cwebsite,
  37. css: css,
  38. instance: instance,
  39. postFormats: postFormats,
  40. renderer: renderer,
  41. }
  42. }
  43. func (s *service) cdata(c *client, title string, count int, rinterval int,
  44. target string) (data *renderer.CommonData) {
  45. data = &renderer.CommonData{
  46. Title: title + " - " + s.cname,
  47. CustomCSS: s.css,
  48. Count: count,
  49. RefreshInterval: rinterval,
  50. Target: target,
  51. }
  52. if c != nil && c.s.IsLoggedIn() {
  53. data.CSRFToken = c.s.CSRFToken
  54. }
  55. return
  56. }
  57. func (s *service) ErrorPage(c *client, err error, retry bool) error {
  58. var errStr string
  59. var sessionErr bool
  60. if err != nil {
  61. errStr = err.Error()
  62. if me, ok := err.(mastodon.Error); ok && me.IsAuthError() ||
  63. err == errInvalidSession || err == errInvalidCSRFToken {
  64. sessionErr = true
  65. }
  66. }
  67. cdata := s.cdata(nil, "error", 0, 0, "")
  68. data := &renderer.ErrorData{
  69. CommonData: cdata,
  70. Err: errStr,
  71. Retry: retry,
  72. SessionErr: sessionErr,
  73. }
  74. return s.renderer.Render(c.rctx, c.w, renderer.ErrorPage, data)
  75. }
  76. func (s *service) SigninPage(c *client) (err error) {
  77. cdata := s.cdata(nil, "signin", 0, 0, "")
  78. data := &renderer.SigninData{
  79. CommonData: cdata,
  80. }
  81. return s.renderer.Render(c.rctx, c.w, renderer.SigninPage, data)
  82. }
  83. func (s *service) RootPage(c *client) (err error) {
  84. data := &renderer.RootData{
  85. Title: s.cname,
  86. }
  87. return s.renderer.Render(c.rctx, c.w, renderer.RootPage, data)
  88. }
  89. func (s *service) NavPage(c *client) (err error) {
  90. u, err := c.GetAccountCurrentUser(c.ctx)
  91. if err != nil {
  92. return
  93. }
  94. pctx := model.PostContext{
  95. DefaultVisibility: c.s.Settings.DefaultVisibility,
  96. DefaultFormat: c.s.Settings.DefaultFormat,
  97. Formats: s.postFormats,
  98. }
  99. cdata := s.cdata(c, "nav", 0, 0, "main")
  100. data := &renderer.NavData{
  101. User: u,
  102. CommonData: cdata,
  103. PostContext: pctx,
  104. }
  105. return s.renderer.Render(c.rctx, c.w, renderer.NavPage, data)
  106. }
  107. func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
  108. minID string, tag string, instance_type string) (err error) {
  109. var nextLink, prevLink, title string
  110. var statuses []*mastodon.Status
  111. var reactions []*mastodon.ReactionsPleroma
  112. var pg = mastodon.Pagination{
  113. MaxID: maxID,
  114. MinID: minID,
  115. Limit: 20,
  116. }
  117. switch tType {
  118. default:
  119. return errInvalidArgument
  120. case "home":
  121. statuses, err = c.GetTimelineHome(c.ctx, &pg)
  122. if err != nil {
  123. return err
  124. }
  125. title = "Timeline"
  126. case "direct":
  127. statuses, err = c.GetTimelineDirect(c.ctx, &pg)
  128. if err != nil {
  129. return err
  130. }
  131. title = "Direct Timeline"
  132. case "local":
  133. statuses, err = c.GetTimelinePublic(c.ctx, true, "", &pg)
  134. if err != nil {
  135. return err
  136. }
  137. title = "Local Timeline"
  138. case "remote":
  139. if len(instance) > 0 {
  140. statuses, err = c.GetTimelinePublic(c.ctx, true, instance, &pg)
  141. if err != nil {
  142. return err
  143. }
  144. }
  145. title = "Remote Timeline"
  146. case "tremote":
  147. if len(instance) > 0 {
  148. if instance_type == "" {
  149. instance_type = "mastodon-compatible"
  150. }
  151. statuses, err = c.TrueRemoteTimeline(c.ctx, instance, instance_type, &pg)
  152. if err != nil {
  153. return err
  154. }
  155. v := make(url.Values)
  156. v.Set("max_id", statuses[len(statuses)-1].ID)
  157. v.Set("instance", instance)
  158. v.Set("instance_type", instance_type)
  159. nextLink = "/timeline/" + tType + "?" + v.Encode()
  160. }
  161. title = "True Remote Timeline"
  162. case "twkn":
  163. statuses, err = c.GetTimelinePublic(c.ctx, false, "", &pg)
  164. if err != nil {
  165. return err
  166. }
  167. title = "The Whole Known Network"
  168. case "tag":
  169. if len(tag) > 0 {
  170. statuses, err = c.GetTimelineHashtag(c.ctx, tag, false, &pg)
  171. if err != nil {
  172. return err
  173. }
  174. }
  175. title = "Tag timeline"
  176. case "list":
  177. statuses, err = c.GetTimelineList(c.ctx, listId, &pg)
  178. if err != nil {
  179. return err
  180. }
  181. list, err := c.GetList(c.ctx, listId)
  182. if err != nil {
  183. return err
  184. }
  185. title = "List Timeline - " + list.Title
  186. }
  187. for i := range statuses {
  188. if statuses[i].Reblog != nil {
  189. statuses[i].Reblog.RetweetedByID = statuses[i].ID
  190. }
  191. if statuses[i].Pleroma.Reactions == nil && statuses[i].Account.Pleroma != nil {
  192. if statuses[i].Reblog != nil {
  193. reactions, err = c.GetReactedBy(c.ctx, statuses[i].Reblog.ID)
  194. } else {
  195. reactions, err = c.GetReactedBy(c.ctx, statuses[i].ID)
  196. }
  197. if err != nil {
  198. return err
  199. }
  200. statuses[i].Pleroma.Reactions = reactions
  201. }
  202. if tType == "tremote" {
  203. statuses[i].URL = fmt.Sprintf("/search?q=%s&type=statuses", statuses[i].URL)
  204. }
  205. }
  206. if (len(maxID) > 0 || len(minID) > 0) && len(statuses) > 0 {
  207. v := make(url.Values)
  208. v.Set("min_id", statuses[0].ID)
  209. if len(instance) > 0 {
  210. v.Set("instance", instance)
  211. }
  212. if len(instance_type) > 0 {
  213. v.Set("instance_type", instance_type)
  214. }
  215. if len(tag) > 0 {
  216. v.Set("tag", tag)
  217. }
  218. if len(listId) > 0 {
  219. v.Set("list", listId)
  220. }
  221. prevLink = "/timeline/" + tType + "?" + v.Encode()
  222. }
  223. if len(minID) > 0 || (len(pg.MaxID) > 0 && len(statuses) == 20) {
  224. v := make(url.Values)
  225. v.Set("max_id", pg.MaxID)
  226. v.Set("max_id", statuses[len(statuses)-1].ID)
  227. if len(instance) > 0 {
  228. v.Set("instance", instance)
  229. }
  230. if len(instance_type) > 0 {
  231. v.Set("instance_type", instance_type)
  232. }
  233. if len(tag) > 0 {
  234. v.Set("tag", tag)
  235. }
  236. if len(listId) > 0 {
  237. v.Set("list", listId)
  238. }
  239. nextLink = "/timeline/" + tType + "?" + v.Encode()
  240. }
  241. cdata := s.cdata(c, tType+" timeline ", 0, 0, "")
  242. data := &renderer.TimelineData{
  243. Title: title,
  244. Type: tType,
  245. Instance: instance,
  246. InstanceType: instance_type,
  247. Statuses: statuses,
  248. NextLink: nextLink,
  249. PrevLink: prevLink,
  250. CommonData: cdata,
  251. }
  252. return s.renderer.Render(c.rctx, c.w, renderer.TimelinePage, data)
  253. }
  254. func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{},
  255. val string, number int) {
  256. if key == nil {
  257. return
  258. }
  259. keyStr, ok := key.(string)
  260. if !ok {
  261. return
  262. }
  263. _, ok = m[keyStr]
  264. if !ok {
  265. m[keyStr] = []mastodon.ReplyInfo{}
  266. }
  267. m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
  268. }
  269. func (s *service) ListsPage(c *client) (err error) {
  270. lists, err := c.GetLists(c.ctx)
  271. if err != nil {
  272. return
  273. }
  274. cdata := s.cdata(c, "Lists", 0, 0, "")
  275. data := renderer.ListsData{
  276. Lists: lists,
  277. CommonData: cdata,
  278. }
  279. return s.renderer.Render(c.rctx, c.w, renderer.ListsPage, data)
  280. }
  281. func (s *service) AddList(c *client, title string) (err error) {
  282. _, err = c.CreateList(c.ctx, title)
  283. return err
  284. }
  285. func (s *service) RemoveList(c *client, id string) (err error) {
  286. return c.DeleteList(c.ctx, id)
  287. }
  288. func (s *service) RenameList(c *client, id, title string) (err error) {
  289. _, err = c.RenameList(c.ctx, id, title)
  290. return err
  291. }
  292. func (s *service) ListPage(c *client, id string, q string) (err error) {
  293. list, err := c.GetList(c.ctx, id)
  294. if err != nil {
  295. return
  296. }
  297. accounts, err := c.GetListAccounts(c.ctx, id)
  298. if err != nil {
  299. return
  300. }
  301. var searchAccounts []*mastodon.Account
  302. if len(q) > 0 {
  303. result, err := c.Search(c.ctx, q, "accounts", 20, true, 0, id, true)
  304. if err != nil {
  305. return err
  306. }
  307. searchAccounts = result.Accounts
  308. }
  309. cdata := s.cdata(c, "List "+list.Title, 0, 0, "")
  310. data := renderer.ListData{
  311. List: list,
  312. Accounts: accounts,
  313. Q: q,
  314. SearchAccounts: searchAccounts,
  315. CommonData: cdata,
  316. }
  317. return s.renderer.Render(c.rctx, c.w, renderer.ListPage, data)
  318. }
  319. func (s *service) ListAddUser(c *client, id string, uid string) (err error) {
  320. return c.AddToList(c.ctx, id, uid)
  321. }
  322. func (s *service) ListRemoveUser(c *client, id string, uid string) (err error) {
  323. return c.RemoveFromList(c.ctx, id, uid)
  324. }
  325. func (s *service) ThreadPage(c *client, id string, reply bool) (err error) {
  326. var pctx model.PostContext
  327. status, err := c.GetStatus(c.ctx, id)
  328. if err != nil {
  329. return
  330. }
  331. if reply {
  332. var spoilerText string
  333. var content string
  334. var visibility string
  335. spoilerText = status.SpoilerText
  336. if c.s.UserID != status.Account.ID {
  337. content += "@" + status.Account.Acct + " "
  338. }
  339. for i := range status.Mentions {
  340. if status.Mentions[i].ID != c.s.UserID &&
  341. status.Mentions[i].ID != status.Account.ID {
  342. content += "@" + status.Mentions[i].Acct + " "
  343. }
  344. }
  345. isDirect := status.Visibility == "direct"
  346. if isDirect || c.s.Settings.CopyScope {
  347. visibility = status.Visibility
  348. } else {
  349. visibility = c.s.Settings.DefaultVisibility
  350. }
  351. replyLanguage := status.Language
  352. pctx = model.PostContext{
  353. DefaultVisibility: visibility,
  354. DefaultFormat: c.s.Settings.DefaultFormat,
  355. Formats: s.postFormats,
  356. ReplyContext: &model.ReplyContext{
  357. InReplyToID: id,
  358. InReplyToName: status.Account.Acct,
  359. ReplySpoiler: spoilerText,
  360. ReplyContent: content,
  361. ForceVisibility: isDirect,
  362. ReplyLanguage: replyLanguage,
  363. },
  364. }
  365. }
  366. context, err := c.GetStatusContext(c.ctx, id)
  367. if err != nil {
  368. return
  369. }
  370. statuses := append(append(context.Ancestors, status), context.Descendants...)
  371. replies := make(map[string][]mastodon.ReplyInfo)
  372. idNumbers := make(map[string]int)
  373. for i := range statuses {
  374. statuses[i].ShowReplies = true
  375. statuses[i].IDNumbers = idNumbers
  376. idNumbers[statuses[i].ID] = i + 1
  377. statuses[i].IDReplies = replies
  378. addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)
  379. }
  380. cdata := s.cdata(c, "post by "+status.Account.DisplayName, 0, 0, "")
  381. data := &renderer.ThreadData{
  382. Statuses: statuses,
  383. PostContext: pctx,
  384. ReplyMap: replies,
  385. CommonData: cdata,
  386. }
  387. return s.renderer.Render(c.rctx, c.w, renderer.ThreadPage, data)
  388. }
  389. func (s *service) QuickReplyPage(c *client, id string) (err error) {
  390. status, err := c.GetStatus(c.ctx, id)
  391. if err != nil {
  392. return
  393. }
  394. var ancestor *mastodon.Status
  395. if status.InReplyToID != nil {
  396. ancestor, err = c.GetStatus(c.ctx, status.InReplyToID.(string))
  397. if err != nil {
  398. return
  399. }
  400. }
  401. var content string
  402. if c.s.UserID != status.Account.ID {
  403. content += "@" + status.Account.Acct + " "
  404. }
  405. for i := range status.Mentions {
  406. if status.Mentions[i].ID != c.s.UserID &&
  407. status.Mentions[i].ID != status.Account.ID {
  408. content += "@" + status.Mentions[i].Acct + " "
  409. }
  410. }
  411. var visibility string
  412. isDirect := status.Visibility == "direct"
  413. if isDirect || c.s.Settings.CopyScope {
  414. visibility = status.Visibility
  415. } else {
  416. visibility = c.s.Settings.DefaultVisibility
  417. }
  418. pctx := model.PostContext{
  419. DefaultVisibility: visibility,
  420. DefaultFormat: c.s.Settings.DefaultFormat,
  421. Formats: s.postFormats,
  422. ReplyContext: &model.ReplyContext{
  423. InReplyToID: id,
  424. InReplyToName: status.Account.Acct,
  425. QuickReply: true,
  426. ReplyContent: content,
  427. ForceVisibility: isDirect,
  428. },
  429. }
  430. cdata := s.cdata(c, "post by "+status.Account.DisplayName, 0, 0, "")
  431. data := &renderer.QuickReplyData{
  432. Ancestor: ancestor,
  433. Status: status,
  434. PostContext: pctx,
  435. CommonData: cdata,
  436. }
  437. return s.renderer.Render(c.rctx, c.w, renderer.QuickReplyPage, data)
  438. }
  439. func (s *service) LikedByPage(c *client, id string) (err error) {
  440. likers, err := c.GetFavouritedBy(c.ctx, id, nil)
  441. if err != nil {
  442. return
  443. }
  444. cdata := s.cdata(c, "likes", 0, 0, "")
  445. data := &renderer.LikedByData{
  446. CommonData: cdata,
  447. Users: likers,
  448. }
  449. return s.renderer.Render(c.rctx, c.w, renderer.LikedByPage, data)
  450. }
  451. func (s *service) ReactedByPage(c *client, id string) (err error) {
  452. reactions, err := c.GetReactedBy(c.ctx, id)
  453. if err != nil {
  454. return
  455. }
  456. cdata := s.cdata(c, "reactions", 0, 0, "")
  457. data := &renderer.ReactedByData{
  458. CommonData: cdata,
  459. ID: id,
  460. Reactions: reactions,
  461. ReactionEmojis: emoji.Map(),
  462. }
  463. return s.renderer.Render(c.rctx, c.w, renderer.ReactedByPage, data)
  464. }
  465. func (s *service) RetweetedByPage(c *client, id string) (err error) {
  466. retweeters, err := c.GetRebloggedBy(c.ctx, id, nil)
  467. if err != nil {
  468. return
  469. }
  470. cdata := s.cdata(c, "retweets", 0, 0, "")
  471. data := &renderer.RetweetedByData{
  472. CommonData: cdata,
  473. Users: retweeters,
  474. }
  475. return s.renderer.Render(c.rctx, c.w, renderer.RetweetedByPage, data)
  476. }
  477. func (s *service) NotificationPage(c *client, maxID string,
  478. minID string) (err error) {
  479. var nextLink string
  480. var unreadCount int
  481. var readID string
  482. var includes, excludes []string
  483. var pg = mastodon.Pagination{
  484. MaxID: maxID,
  485. MinID: minID,
  486. Limit: 20,
  487. }
  488. if c.s.Settings.HideUnsupportedNotifs {
  489. // Explicitly include the supported types.
  490. // For now, only Pleroma supports this option, Mastadon
  491. // will simply ignore the unknown params.
  492. includes = []string{"follow", "follow_request", "mention", "reblog", "favourite"}
  493. }
  494. if c.s.Settings.AntiDopamineMode {
  495. excludes = append(excludes, "follow", "favourite", "reblog")
  496. }
  497. notifications, err := c.GetNotifications(c.ctx, &pg, includes, excludes)
  498. if err != nil {
  499. return
  500. }
  501. for i := range notifications {
  502. if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
  503. unreadCount++
  504. }
  505. }
  506. if unreadCount > 0 {
  507. readID = notifications[0].ID
  508. }
  509. if len(notifications) == 20 && len(pg.MaxID) > 0 {
  510. nextLink = "/notifications?max_id=" + pg.MaxID
  511. }
  512. cdata := s.cdata(c, "notifications", unreadCount,
  513. c.s.Settings.NotificationInterval, "main")
  514. data := &renderer.NotificationData{
  515. Notifications: notifications,
  516. UnreadCount: unreadCount,
  517. ReadID: readID,
  518. NextLink: nextLink,
  519. CommonData: cdata,
  520. }
  521. return s.renderer.Render(c.rctx, c.w, renderer.NotificationPage, data)
  522. }
  523. func (s *service) UserPage(c *client, id string, pageType string,
  524. maxID string, minID string) (err error) {
  525. var nextLink string
  526. var statuses []*mastodon.Status
  527. var users []*mastodon.Account
  528. var pg = mastodon.Pagination{
  529. MaxID: maxID,
  530. MinID: minID,
  531. Limit: 20,
  532. }
  533. user, err := c.GetAccount(c.ctx, id)
  534. if err != nil {
  535. return
  536. }
  537. isCurrent := c.s.UserID == user.ID
  538. switch pageType {
  539. case "":
  540. statuses, err = c.GetAccountStatuses(c.ctx, id, false, false, &pg)
  541. if err != nil {
  542. return
  543. }
  544. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  545. nextLink = fmt.Sprintf("/user/%s?max_id=%s", id,
  546. pg.MaxID)
  547. }
  548. case "following":
  549. users, err = c.GetAccountFollowing(c.ctx, id, &pg)
  550. if err != nil {
  551. return
  552. }
  553. if len(users) == 20 && len(pg.MaxID) > 0 {
  554. nextLink = fmt.Sprintf("/user/%s/following?max_id=%s",
  555. id, pg.MaxID)
  556. }
  557. case "followers":
  558. users, err = c.GetAccountFollowers(c.ctx, id, &pg)
  559. if err != nil {
  560. return
  561. }
  562. if len(users) == 20 && len(pg.MaxID) > 0 {
  563. nextLink = fmt.Sprintf("/user/%s/followers?max_id=%s",
  564. id, pg.MaxID)
  565. }
  566. case "media":
  567. statuses, err = c.GetAccountStatuses(c.ctx, id, true, false, &pg)
  568. if err != nil {
  569. return
  570. }
  571. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  572. nextLink = fmt.Sprintf("/user/%s/media?max_id=%s",
  573. id, pg.MaxID)
  574. }
  575. case "pinned":
  576. statuses, err = c.GetAccountStatuses(c.ctx, id, false, true, &pg)
  577. if err != nil {
  578. return
  579. }
  580. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  581. nextLink = fmt.Sprintf("/user/%s/pinned?max_id=%s",
  582. id, pg.MaxID)
  583. }
  584. case "bookmarks":
  585. if !isCurrent {
  586. return errInvalidArgument
  587. }
  588. statuses, err = c.GetBookmarks(c.ctx, &pg)
  589. if err != nil {
  590. return
  591. }
  592. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  593. nextLink = fmt.Sprintf("/user/%s/bookmarks?max_id=%s",
  594. id, pg.MaxID)
  595. }
  596. case "mutes":
  597. if !isCurrent {
  598. return errInvalidArgument
  599. }
  600. users, err = c.GetMutes(c.ctx, &pg)
  601. if err != nil {
  602. return
  603. }
  604. if len(users) == 20 && len(pg.MaxID) > 0 {
  605. nextLink = fmt.Sprintf("/user/%s/mutes?max_id=%s",
  606. id, pg.MaxID)
  607. }
  608. case "blocks":
  609. if !isCurrent {
  610. return errInvalidArgument
  611. }
  612. users, err = c.GetBlocks(c.ctx, &pg)
  613. if err != nil {
  614. return
  615. }
  616. if len(users) == 20 && len(pg.MaxID) > 0 {
  617. nextLink = fmt.Sprintf("/user/%s/blocks?max_id=%s",
  618. id, pg.MaxID)
  619. }
  620. case "likes":
  621. if !isCurrent {
  622. return errInvalidArgument
  623. }
  624. statuses, err = c.GetFavourites(c.ctx, &pg)
  625. if err != nil {
  626. return
  627. }
  628. if len(statuses) == 20 && len(pg.MaxID) > 0 {
  629. nextLink = fmt.Sprintf("/user/%s/likes?max_id=%s",
  630. id, pg.MaxID)
  631. }
  632. case "requests":
  633. if !isCurrent {
  634. return errInvalidArgument
  635. }
  636. users, err = c.GetFollowRequests(c.ctx, &pg)
  637. if err != nil {
  638. return
  639. }
  640. if len(users) == 20 && len(pg.MaxID) > 0 {
  641. nextLink = fmt.Sprintf("/user/%s/requests?max_id=%s",
  642. id, pg.MaxID)
  643. }
  644. default:
  645. return errInvalidArgument
  646. }
  647. for i := range statuses {
  648. if statuses[i].Reblog != nil {
  649. statuses[i].Reblog.RetweetedByID = statuses[i].ID
  650. }
  651. }
  652. cdata := s.cdata(c, user.DisplayName+" @"+user.Acct, 0, 0, "")
  653. data := &renderer.UserData{
  654. User: user,
  655. IsCurrent: isCurrent,
  656. Type: pageType,
  657. Users: users,
  658. Statuses: statuses,
  659. NextLink: nextLink,
  660. CommonData: cdata,
  661. }
  662. return s.renderer.Render(c.rctx, c.w, renderer.UserPage, data)
  663. }
  664. func (s *service) UserSearchPage(c *client,
  665. id string, q string, offset int) (err error) {
  666. var nextLink string
  667. var title = "search"
  668. user, err := c.GetAccount(c.ctx, id)
  669. if err != nil {
  670. return
  671. }
  672. var results *mastodon.Results
  673. if len(q) > 0 {
  674. results, err = c.Search(c.ctx, q, "statuses", 20, true, offset, id, false)
  675. if err != nil {
  676. return err
  677. }
  678. } else {
  679. results = &mastodon.Results{}
  680. }
  681. if len(results.Statuses) == 20 {
  682. offset += 20
  683. nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id,
  684. q, offset)
  685. }
  686. if len(q) > 0 {
  687. title += " \"" + q + "\""
  688. }
  689. cdata := s.cdata(c, title, 0, 0, "")
  690. data := &renderer.UserSearchData{
  691. CommonData: cdata,
  692. User: user,
  693. Q: q,
  694. Statuses: results.Statuses,
  695. NextLink: nextLink,
  696. }
  697. return s.renderer.Render(c.rctx, c.w, renderer.UserSearchPage, data)
  698. }
  699. func (s *service) MutePage(c *client, id string) (err error) {
  700. user, err := c.GetAccount(c.ctx, id)
  701. if err != nil {
  702. return
  703. }
  704. cdata := s.cdata(c, "Mute "+user.DisplayName+" @"+user.Acct, 0, 0, "")
  705. data := &renderer.UserData{
  706. User: user,
  707. CommonData: cdata,
  708. }
  709. return s.renderer.Render(c.rctx, c.w, renderer.MutePage, data)
  710. }
  711. func (s *service) AboutPage(c *client) (err error) {
  712. cdata := s.cdata(c, "about", 0, 0, "")
  713. data := &renderer.AboutData{
  714. CommonData: cdata,
  715. }
  716. return s.renderer.Render(c.rctx, c.w, renderer.AboutPage, data)
  717. }
  718. func (s *service) AboutInstance(c *client) (err error) {
  719. cdata := s.cdata(c, "about", 0, 0, "")
  720. instance, err := c.GetInstance(c.ctx)
  721. if err != nil {
  722. return
  723. }
  724. peers, err := c.GetInstancePeers(c.ctx)
  725. if err != nil {
  726. return
  727. }
  728. data := &renderer.AboutInstanceData{
  729. CommonData: cdata,
  730. Instance: instance,
  731. Peers: peers,
  732. }
  733. return s.renderer.Render(c.rctx, c.w, renderer.AboutInstance, data)
  734. }
  735. func (s *service) EmojiPage(c *client) (err error) {
  736. emojis, err := c.GetInstanceEmojis(c.ctx)
  737. if err != nil {
  738. return
  739. }
  740. cdata := s.cdata(c, "emojis", 0, 0, "")
  741. data := &renderer.EmojiData{
  742. Emojis: emojis,
  743. CommonData: cdata,
  744. }
  745. return s.renderer.Render(c.rctx, c.w, renderer.EmojiPage, data)
  746. }
  747. func (s *service) SearchPage(c *client,
  748. q string, qType string, offset int) (err error) {
  749. var nextLink string
  750. var title = "search"
  751. var results *mastodon.Results
  752. if len(q) > 0 {
  753. results, err = c.Search(c.ctx, q, qType, 20, true, offset, "", false)
  754. if err != nil {
  755. return err
  756. }
  757. } else {
  758. results = &mastodon.Results{}
  759. }
  760. if (qType == "accounts" && len(results.Accounts) == 20) ||
  761. (qType == "statuses" && len(results.Statuses) == 20) {
  762. offset += 20
  763. nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d",
  764. q, qType, offset)
  765. }
  766. if len(q) > 0 {
  767. title += " \"" + q + "\""
  768. }
  769. cdata := s.cdata(c, title, 0, 0, "")
  770. data := &renderer.SearchData{
  771. CommonData: cdata,
  772. Q: q,
  773. Type: qType,
  774. Users: results.Accounts,
  775. Statuses: results.Statuses,
  776. NextLink: nextLink,
  777. }
  778. return s.renderer.Render(c.rctx, c.w, renderer.SearchPage, data)
  779. }
  780. func (s *service) SettingsPage(c *client) (err error) {
  781. cdata := s.cdata(c, "settings", 0, 0, "")
  782. data := &renderer.SettingsData{
  783. CommonData: cdata,
  784. Settings: &c.s.Settings,
  785. PostFormats: s.postFormats,
  786. }
  787. return s.renderer.Render(c.rctx, c.w, renderer.SettingsPage, data)
  788. }
  789. func (svc *service) FiltersPage(c *client) (err error) {
  790. filters, err := c.GetFilters(c.ctx)
  791. if err != nil {
  792. return
  793. }
  794. cdata := svc.cdata(c, "filters", 0, 0, "")
  795. data := &renderer.FiltersData{
  796. CommonData: cdata,
  797. Filters: filters,
  798. }
  799. return svc.renderer.Render(c.rctx, c.w, renderer.FiltersPage, data)
  800. }
  801. func (svc *service) ProfilePage(c *client) (err error) {
  802. u, err := c.GetAccountCurrentUser(c.ctx)
  803. if err != nil {
  804. return
  805. }
  806. // Some instances allow more than 4 fields, but make sure that there are
  807. // at least 4 fields in the slice because the template depends on it.
  808. if u.Source.Fields == nil {
  809. u.Source.Fields = new([]mastodon.Field)
  810. }
  811. for len(*u.Source.Fields) < 4 {
  812. *u.Source.Fields = append(*u.Source.Fields, mastodon.Field{})
  813. }
  814. cdata := svc.cdata(c, "edit profile", 0, 0, "")
  815. data := &renderer.ProfileData{
  816. CommonData: cdata,
  817. User: u,
  818. }
  819. return svc.renderer.Render(c.rctx, c.w, renderer.ProfilePage, data)
  820. }
  821. func (s *service) ProfileUpdate(c *client, name, bio string, avatar, banner *multipart.FileHeader,
  822. fields []mastodon.Field, locked bool) (err error) {
  823. // Need to pass empty data to clear fields
  824. if len(fields) == 0 {
  825. fields = append(fields, mastodon.Field{})
  826. }
  827. p := &mastodon.Profile{
  828. DisplayName: &name,
  829. Note: &bio,
  830. Avatar: avatar,
  831. Header: banner,
  832. Fields: &fields,
  833. Locked: &locked,
  834. }
  835. _, err = c.AccountUpdate(c.ctx, p)
  836. return err
  837. }
  838. func (s *service) ProfileDelAvatar(c *client) (err error) {
  839. _, err = c.AccountDeleteAvatar(c.ctx)
  840. return
  841. }
  842. func (s *service) ProfileDelBanner(c *client) (err error) {
  843. _, err = c.AccountDeleteHeader(c.ctx)
  844. return err
  845. }
  846. func (s *service) SingleInstance() (instance string, ok bool) {
  847. if len(s.instance) > 0 {
  848. instance = s.instance
  849. ok = true
  850. }
  851. return
  852. }
  853. func (s *service) NewSession(c *client, instance string) (rurl string, sess *model.Session, err error) {
  854. var instanceURL string
  855. if strings.HasPrefix(instance, "https://") {
  856. instanceURL = instance
  857. instance = strings.TrimPrefix(instance, "https://")
  858. } else {
  859. instanceURL = "https://" + instance
  860. }
  861. csrf, err := util.NewCSRFToken()
  862. if err != nil {
  863. return
  864. }
  865. app, err := mastodon.RegisterApp(c.ctx, &mastodon.AppConfig{
  866. Server: instanceURL,
  867. ClientName: s.cname,
  868. Scopes: s.cscope,
  869. Website: s.cwebsite,
  870. RedirectURIs: s.cwebsite + "/oauth_callback",
  871. })
  872. if err != nil {
  873. return
  874. }
  875. rurl = app.AuthURI
  876. sess = &model.Session{
  877. Instance: instance,
  878. ClientID: app.ClientID,
  879. ClientSecret: app.ClientSecret,
  880. CSRFToken: csrf,
  881. Settings: *model.NewSettings(),
  882. }
  883. return
  884. }
  885. func (s *service) Signin(c *client, code string) (err error) {
  886. if len(code) < 1 {
  887. err = errInvalidArgument
  888. return
  889. }
  890. err = c.AuthenticateToken(c.ctx, code, s.cwebsite+"/oauth_callback")
  891. if err != nil {
  892. return
  893. }
  894. u, err := c.GetAccountCurrentUser(c.ctx)
  895. if err != nil {
  896. return
  897. }
  898. c.s.AccessToken = c.GetAccessToken(c.ctx)
  899. c.s.UserID = u.ID
  900. return c.setSession(c.s)
  901. }
  902. func (s *service) Signout(c *client) (err error) {
  903. return c.RevokeToken(c.ctx)
  904. }
  905. func (s *service) Post(c *client, content string, replyToID string,
  906. format string, visibility string, isNSFW bool, spoilerText string,
  907. files []*multipart.FileHeader, edit string, language string, expiresIn int, scheduledAt string,
  908. pollOptions []string, pollExpiresIn int, pollHideTotals bool, pollMultiple bool,
  909. mediaDescription []string) (id string, err error) {
  910. var mediaIDs []string
  911. for idx, f := range files {
  912. a, err := c.UploadMediaFromMultipartFileHeader(c.ctx, f, mediaDescription[idx])
  913. if err != nil {
  914. return "", err
  915. }
  916. mediaIDs = append(mediaIDs, a.ID)
  917. }
  918. expiresIn = expiresIn * 3600
  919. pollTweet := mastodon.TootPoll{
  920. ExpiresIn: pollExpiresIn,
  921. Options: pollOptions,
  922. Multiple: pollMultiple,
  923. HideTotals: pollHideTotals,
  924. }
  925. tweet := &mastodon.Toot{
  926. SpoilerText: spoilerText,
  927. Status: content,
  928. InReplyToID: replyToID,
  929. MediaIDs: mediaIDs,
  930. ContentType: format,
  931. Visibility: visibility,
  932. Sensitive: isNSFW,
  933. Edit: edit,
  934. Language: language,
  935. ExpiresIn: expiresIn, // pleroma compatible
  936. ScheduledAt: scheduledAt,
  937. Poll: pollTweet,
  938. }
  939. st, err := c.PostStatus(c.ctx, tweet)
  940. if err != nil {
  941. return
  942. }
  943. return st.ID, nil
  944. }
  945. func (s *service) Like(c *client, id string) (count int64, err error) {
  946. st, err := c.Favourite(c.ctx, id)
  947. if err != nil {
  948. return
  949. }
  950. count = st.FavouritesCount
  951. return
  952. }
  953. func (s *service) UnLike(c *client, id string) (count int64, err error) {
  954. st, err := c.Unfavourite(c.ctx, id)
  955. if err != nil {
  956. return
  957. }
  958. count = st.FavouritesCount
  959. return
  960. }
  961. func (s *service) PutReact(c *client, id string, emoji string) (count int64, err error) {
  962. st, err := c.PutReaction(c.ctx, id, emoji)
  963. if err != nil {
  964. return
  965. }
  966. count = st.FavouritesCount
  967. return
  968. }
  969. func (s *service) UnReact(c *client, id string, emoji string) (count int64, err error) {
  970. st, err := c.UnReaction(c.ctx, id, emoji)
  971. if err != nil {
  972. return
  973. }
  974. count = st.FavouritesCount
  975. return
  976. }
  977. func (s *service) Retweet(c *client, id string, visibility string) (count int64, err error) {
  978. st, err := c.Reblog(c.ctx, id, visibility)
  979. if err != nil {
  980. return
  981. }
  982. if st.Reblog != nil {
  983. count = st.Reblog.ReblogsCount
  984. }
  985. return
  986. }
  987. func (s *service) UnRetweet(c *client, id string) (
  988. count int64, err error) {
  989. st, err := c.Unreblog(c.ctx, id)
  990. if err != nil {
  991. return
  992. }
  993. count = st.ReblogsCount
  994. return
  995. }
  996. func (s *service) Vote(c *client, id string, choices []string) (err error) {
  997. _, err = c.Vote(c.ctx, id, choices)
  998. return
  999. }
  1000. func (s *service) Follow(c *client, id string, reblogs *bool) (err error) {
  1001. _, err = c.AccountFollow(c.ctx, id, reblogs)
  1002. return
  1003. }
  1004. func (s *service) UnFollow(c *client, id string) (err error) {
  1005. _, err = c.AccountUnfollow(c.ctx, id)
  1006. return
  1007. }
  1008. func (s *service) Accept(c *client, id string) (err error) {
  1009. return c.FollowRequestAuthorize(c.ctx, id)
  1010. }
  1011. func (s *service) Reject(c *client, id string) (err error) {
  1012. return c.FollowRequestReject(c.ctx, id)
  1013. }
  1014. func (s *service) Mute(c *client, id string, notifications bool, duration int) (err error) {
  1015. _, err = c.AccountMute(c.ctx, id, notifications, duration)
  1016. return
  1017. }
  1018. func (s *service) UnMute(c *client, id string) (err error) {
  1019. _, err = c.AccountUnmute(c.ctx, id)
  1020. return
  1021. }
  1022. func (s *service) Block(c *client, id string) (err error) {
  1023. _, err = c.AccountBlock(c.ctx, id)
  1024. return
  1025. }
  1026. func (s *service) UnBlock(c *client, id string) (err error) {
  1027. _, err = c.AccountUnblock(c.ctx, id)
  1028. return
  1029. }
  1030. func (s *service) Subscribe(c *client, id string) (err error) {
  1031. _, err = c.Subscribe(c.ctx, id)
  1032. return
  1033. }
  1034. func (s *service) UnSubscribe(c *client, id string) (err error) {
  1035. _, err = c.UnSubscribe(c.ctx, id)
  1036. return
  1037. }
  1038. func (s *service) SaveSettings(c *client, settings *model.Settings) (err error) {
  1039. switch settings.NotificationInterval {
  1040. case 0, 30, 60, 120, 300, 600:
  1041. default:
  1042. return errInvalidArgument
  1043. }
  1044. if len(settings.CSS) > 0 {
  1045. if len(settings.CSS) > 1<<20 {
  1046. return errInvalidArgument
  1047. }
  1048. // For some reason, browsers convert CRLF to LF before calculating
  1049. // the hash of the inline resources.
  1050. settings.CSS = strings.Replace(settings.CSS, "\x0d\x0a", "\x0a", -1)
  1051. h := sha256.Sum256([]byte(settings.CSS))
  1052. settings.CSSHash = base64.StdEncoding.EncodeToString(h[:])
  1053. } else {
  1054. settings.CSSHash = ""
  1055. }
  1056. c.s.Settings = *settings
  1057. return c.setSession(c.s)
  1058. }
  1059. func (s *service) UserSave(c *client, usersettings mastodon.Profile) (err error) {
  1060. _, err = c.AccountUpdate(c.ctx, &usersettings)
  1061. return
  1062. }
  1063. func (s *service) MuteConversation(c *client, id string) (err error) {
  1064. _, err = c.MuteConversation(c.ctx, id)
  1065. return
  1066. }
  1067. func (s *service) UnMuteConversation(c *client, id string) (err error) {
  1068. _, err = c.UnmuteConversation(c.ctx, id)
  1069. return
  1070. }
  1071. func (s *service) Pin(c *client, id string) (err error) {
  1072. return c.Pin(c.ctx, id)
  1073. }
  1074. func (s *service) UnPin(c *client, id string) (err error) {
  1075. return c.UnPin(c.ctx, id)
  1076. }
  1077. func (s *service) Delete(c *client, id string) (err error) {
  1078. return c.DeleteStatus(c.ctx, id)
  1079. }
  1080. func (s *service) ReadNotifications(c *client, maxID string) (err error) {
  1081. return c.ReadNotifications(c.ctx, maxID)
  1082. }
  1083. func (s *service) Bookmark(c *client, id string) (err error) {
  1084. _, err = c.Bookmark(c.ctx, id)
  1085. return
  1086. }
  1087. func (s *service) UnBookmark(c *client, id string) (err error) {
  1088. _, err = c.Unbookmark(c.ctx, id)
  1089. return
  1090. }
  1091. func (svc *service) Filter(c *client, phrase string, wholeWord bool) (err error) {
  1092. fctx := []string{"home", "notifications", "public", "thread"}
  1093. return c.AddFilter(c.ctx, phrase, fctx, true, wholeWord, nil)
  1094. }
  1095. func (svc *service) UnFilter(c *client, id string) (err error) {
  1096. return c.RemoveFilter(c.ctx, id)
  1097. }