mirror of
https://gitea.phreedom.club/localhost_frssoft/bloat.git
synced 2024-11-15 01:09:23 +02:00
67b13c71ba
This helps mitigate XSS exploits. Users will have to save the settings again to make the custom CSS work.
1072 lines
25 KiB
Go
1072 lines
25 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"bloat/mastodon"
|
|
"bloat/model"
|
|
"bloat/renderer"
|
|
"bloat/util"
|
|
)
|
|
|
|
var (
|
|
errInvalidArgument = errors.New("invalid argument")
|
|
errInvalidSession = errors.New("invalid session")
|
|
errInvalidCSRFToken = errors.New("invalid csrf token")
|
|
)
|
|
|
|
type service struct {
|
|
cname string
|
|
cscope string
|
|
cwebsite string
|
|
css string
|
|
instance string
|
|
postFormats []model.PostFormat
|
|
renderer renderer.Renderer
|
|
}
|
|
|
|
func NewService(cname string, cscope string, cwebsite string,
|
|
css string, instance string, postFormats []model.PostFormat,
|
|
renderer renderer.Renderer) *service {
|
|
return &service{
|
|
cname: cname,
|
|
cscope: cscope,
|
|
cwebsite: cwebsite,
|
|
css: css,
|
|
instance: instance,
|
|
postFormats: postFormats,
|
|
renderer: renderer,
|
|
}
|
|
}
|
|
|
|
func (s *service) cdata(c *client, title string, count int, rinterval int,
|
|
target string) (data *renderer.CommonData) {
|
|
data = &renderer.CommonData{
|
|
Title: title + " - " + s.cname,
|
|
CustomCSS: s.css,
|
|
Count: count,
|
|
RefreshInterval: rinterval,
|
|
Target: target,
|
|
}
|
|
if c != nil && c.s.IsLoggedIn() {
|
|
data.CSRFToken = c.s.CSRFToken
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *service) ErrorPage(c *client, err error, retry bool) error {
|
|
var errStr string
|
|
var sessionErr bool
|
|
if err != nil {
|
|
errStr = err.Error()
|
|
if me, ok := err.(mastodon.Error); ok && me.IsAuthError() ||
|
|
err == errInvalidSession || err == errInvalidCSRFToken {
|
|
sessionErr = true
|
|
}
|
|
}
|
|
cdata := s.cdata(nil, "error", 0, 0, "")
|
|
data := &renderer.ErrorData{
|
|
CommonData: cdata,
|
|
Err: errStr,
|
|
Retry: retry,
|
|
SessionErr: sessionErr,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.ErrorPage, data)
|
|
}
|
|
|
|
func (s *service) SigninPage(c *client) (err error) {
|
|
cdata := s.cdata(nil, "signin", 0, 0, "")
|
|
data := &renderer.SigninData{
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.SigninPage, data)
|
|
}
|
|
|
|
func (s *service) RootPage(c *client) (err error) {
|
|
data := &renderer.RootData{
|
|
Title: s.cname,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.RootPage, data)
|
|
}
|
|
|
|
func (s *service) NavPage(c *client) (err error) {
|
|
u, err := c.GetAccountCurrentUser(c.ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
pctx := model.PostContext{
|
|
DefaultVisibility: c.s.Settings.DefaultVisibility,
|
|
DefaultFormat: c.s.Settings.DefaultFormat,
|
|
Formats: s.postFormats,
|
|
}
|
|
cdata := s.cdata(c, "nav", 0, 0, "main")
|
|
data := &renderer.NavData{
|
|
User: u,
|
|
CommonData: cdata,
|
|
PostContext: pctx,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.NavPage, data)
|
|
}
|
|
|
|
func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
|
|
minID string) (err error) {
|
|
|
|
var nextLink, prevLink, title string
|
|
var statuses []*mastodon.Status
|
|
var pg = mastodon.Pagination{
|
|
MaxID: maxID,
|
|
MinID: minID,
|
|
Limit: 20,
|
|
}
|
|
|
|
switch tType {
|
|
default:
|
|
return errInvalidArgument
|
|
case "home":
|
|
statuses, err = c.GetTimelineHome(c.ctx, &pg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
title = "Timeline"
|
|
case "direct":
|
|
statuses, err = c.GetTimelineDirect(c.ctx, &pg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
title = "Direct Timeline"
|
|
case "local":
|
|
statuses, err = c.GetTimelinePublic(c.ctx, true, "", &pg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
title = "Local Timeline"
|
|
case "remote":
|
|
if len(instance) > 0 {
|
|
statuses, err = c.GetTimelinePublic(c.ctx, false, instance, &pg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
title = "Remote Timeline"
|
|
case "twkn":
|
|
statuses, err = c.GetTimelinePublic(c.ctx, false, "", &pg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
title = "The Whole Known Network"
|
|
case "list":
|
|
statuses, err = c.GetTimelineList(c.ctx, listId, &pg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
list, err := c.GetList(c.ctx, listId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
title = "List Timeline - " + list.Title
|
|
}
|
|
|
|
for i := range statuses {
|
|
if statuses[i].Reblog != nil {
|
|
statuses[i].Reblog.RetweetedByID = statuses[i].ID
|
|
}
|
|
}
|
|
|
|
if (len(maxID) > 0 || len(minID) > 0) && len(statuses) > 0 {
|
|
v := make(url.Values)
|
|
v.Set("min_id", statuses[0].ID)
|
|
if len(instance) > 0 {
|
|
v.Set("instance", instance)
|
|
}
|
|
if len(listId) > 0 {
|
|
v.Set("list", listId)
|
|
}
|
|
prevLink = "/timeline/" + tType + "?" + v.Encode()
|
|
}
|
|
|
|
if len(minID) > 0 || (len(pg.MaxID) > 0 && len(statuses) == 20) {
|
|
v := make(url.Values)
|
|
v.Set("max_id", pg.MaxID)
|
|
if len(instance) > 0 {
|
|
v.Set("instance", instance)
|
|
}
|
|
if len(listId) > 0 {
|
|
v.Set("list", listId)
|
|
}
|
|
nextLink = "/timeline/" + tType + "?" + v.Encode()
|
|
}
|
|
|
|
cdata := s.cdata(c, tType+" timeline ", 0, 0, "")
|
|
data := &renderer.TimelineData{
|
|
Title: title,
|
|
Type: tType,
|
|
Instance: instance,
|
|
Statuses: statuses,
|
|
NextLink: nextLink,
|
|
PrevLink: prevLink,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.TimelinePage, data)
|
|
}
|
|
|
|
func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{},
|
|
val string, number int) {
|
|
if key == nil {
|
|
return
|
|
}
|
|
keyStr, ok := key.(string)
|
|
if !ok {
|
|
return
|
|
}
|
|
_, ok = m[keyStr]
|
|
if !ok {
|
|
m[keyStr] = []mastodon.ReplyInfo{}
|
|
}
|
|
m[keyStr] = append(m[keyStr], mastodon.ReplyInfo{val, number})
|
|
}
|
|
|
|
func (s *service) ListsPage(c *client) (err error) {
|
|
lists, err := c.GetLists(c.ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
cdata := s.cdata(c, "Lists", 0, 0, "")
|
|
data := renderer.ListsData{
|
|
Lists: lists,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.ListsPage, data)
|
|
}
|
|
|
|
func (s *service) AddList(c *client, title string) (err error) {
|
|
_, err = c.CreateList(c.ctx, title)
|
|
return err
|
|
}
|
|
|
|
func (s *service) RemoveList(c *client, id string) (err error) {
|
|
return c.DeleteList(c.ctx, id)
|
|
}
|
|
|
|
func (s *service) RenameList(c *client, id, title string) (err error) {
|
|
_, err = c.RenameList(c.ctx, id, title)
|
|
return err
|
|
}
|
|
|
|
func (s *service) ListPage(c *client, id string, q string) (err error) {
|
|
list, err := c.GetList(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
accounts, err := c.GetListAccounts(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var searchAccounts []*mastodon.Account
|
|
if len(q) > 0 {
|
|
result, err := c.Search(c.ctx, q, "accounts", 20, true, 0, id, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
searchAccounts = result.Accounts
|
|
}
|
|
cdata := s.cdata(c, "List "+list.Title, 0, 0, "")
|
|
data := renderer.ListData{
|
|
List: list,
|
|
Accounts: accounts,
|
|
Q: q,
|
|
SearchAccounts: searchAccounts,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.ListPage, data)
|
|
}
|
|
|
|
func (s *service) ListAddUser(c *client, id string, uid string) (err error) {
|
|
return c.AddToList(c.ctx, id, uid)
|
|
}
|
|
|
|
func (s *service) ListRemoveUser(c *client, id string, uid string) (err error) {
|
|
return c.RemoveFromList(c.ctx, id, uid)
|
|
}
|
|
|
|
func (s *service) ThreadPage(c *client, id string, reply bool) (err error) {
|
|
var pctx model.PostContext
|
|
|
|
status, err := c.GetStatus(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if reply {
|
|
var content string
|
|
var visibility string
|
|
if c.s.UserID != status.Account.ID {
|
|
content += "@" + status.Account.Acct + " "
|
|
}
|
|
for i := range status.Mentions {
|
|
if status.Mentions[i].ID != c.s.UserID &&
|
|
status.Mentions[i].ID != status.Account.ID {
|
|
content += "@" + status.Mentions[i].Acct + " "
|
|
}
|
|
}
|
|
|
|
isDirect := status.Visibility == "direct"
|
|
if isDirect || c.s.Settings.CopyScope {
|
|
visibility = status.Visibility
|
|
} else {
|
|
visibility = c.s.Settings.DefaultVisibility
|
|
}
|
|
|
|
pctx = model.PostContext{
|
|
DefaultVisibility: visibility,
|
|
DefaultFormat: c.s.Settings.DefaultFormat,
|
|
Formats: s.postFormats,
|
|
ReplyContext: &model.ReplyContext{
|
|
InReplyToID: id,
|
|
InReplyToName: status.Account.Acct,
|
|
ReplyContent: content,
|
|
ForceVisibility: isDirect,
|
|
},
|
|
}
|
|
}
|
|
|
|
context, err := c.GetStatusContext(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
statuses := append(append(context.Ancestors, status), context.Descendants...)
|
|
replies := make(map[string][]mastodon.ReplyInfo)
|
|
idNumbers := make(map[string]int)
|
|
|
|
for i := range statuses {
|
|
statuses[i].ShowReplies = true
|
|
|
|
statuses[i].IDNumbers = idNumbers
|
|
idNumbers[statuses[i].ID] = i + 1
|
|
|
|
statuses[i].IDReplies = replies
|
|
addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)
|
|
}
|
|
|
|
cdata := s.cdata(c, "post by "+status.Account.DisplayName, 0, 0, "")
|
|
data := &renderer.ThreadData{
|
|
Statuses: statuses,
|
|
PostContext: pctx,
|
|
ReplyMap: replies,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.ThreadPage, data)
|
|
}
|
|
|
|
func (s *service) QuickReplyPage(c *client, id string) (err error) {
|
|
status, err := c.GetStatus(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var ancestor *mastodon.Status
|
|
if status.InReplyToID != nil {
|
|
ancestor, err = c.GetStatus(c.ctx, status.InReplyToID.(string))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
var content string
|
|
if c.s.UserID != status.Account.ID {
|
|
content += "@" + status.Account.Acct + " "
|
|
}
|
|
for i := range status.Mentions {
|
|
if status.Mentions[i].ID != c.s.UserID &&
|
|
status.Mentions[i].ID != status.Account.ID {
|
|
content += "@" + status.Mentions[i].Acct + " "
|
|
}
|
|
}
|
|
|
|
var visibility string
|
|
isDirect := status.Visibility == "direct"
|
|
if isDirect || c.s.Settings.CopyScope {
|
|
visibility = status.Visibility
|
|
} else {
|
|
visibility = c.s.Settings.DefaultVisibility
|
|
}
|
|
|
|
pctx := model.PostContext{
|
|
DefaultVisibility: visibility,
|
|
DefaultFormat: c.s.Settings.DefaultFormat,
|
|
Formats: s.postFormats,
|
|
ReplyContext: &model.ReplyContext{
|
|
InReplyToID: id,
|
|
InReplyToName: status.Account.Acct,
|
|
QuickReply: true,
|
|
ReplyContent: content,
|
|
ForceVisibility: isDirect,
|
|
},
|
|
}
|
|
|
|
cdata := s.cdata(c, "post by "+status.Account.DisplayName, 0, 0, "")
|
|
data := &renderer.QuickReplyData{
|
|
Ancestor: ancestor,
|
|
Status: status,
|
|
PostContext: pctx,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.QuickReplyPage, data)
|
|
}
|
|
|
|
func (s *service) LikedByPage(c *client, id string) (err error) {
|
|
likers, err := c.GetFavouritedBy(c.ctx, id, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
cdata := s.cdata(c, "likes", 0, 0, "")
|
|
data := &renderer.LikedByData{
|
|
CommonData: cdata,
|
|
Users: likers,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.LikedByPage, data)
|
|
}
|
|
|
|
func (s *service) RetweetedByPage(c *client, id string) (err error) {
|
|
retweeters, err := c.GetRebloggedBy(c.ctx, id, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
cdata := s.cdata(c, "retweets", 0, 0, "")
|
|
data := &renderer.RetweetedByData{
|
|
CommonData: cdata,
|
|
Users: retweeters,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.RetweetedByPage, data)
|
|
}
|
|
|
|
func (s *service) NotificationPage(c *client, maxID string,
|
|
minID string) (err error) {
|
|
|
|
var nextLink string
|
|
var unreadCount int
|
|
var readID string
|
|
var includes, excludes []string
|
|
var pg = mastodon.Pagination{
|
|
MaxID: maxID,
|
|
MinID: minID,
|
|
Limit: 20,
|
|
}
|
|
|
|
if c.s.Settings.HideUnsupportedNotifs {
|
|
// Explicitly include the supported types.
|
|
// For now, only Pleroma supports this option, Mastadon
|
|
// will simply ignore the unknown params.
|
|
includes = []string{"follow", "follow_request", "mention", "reblog", "favourite"}
|
|
}
|
|
if c.s.Settings.AntiDopamineMode {
|
|
excludes = append(excludes, "follow", "favourite", "reblog")
|
|
}
|
|
|
|
notifications, err := c.GetNotifications(c.ctx, &pg, includes, excludes)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for i := range notifications {
|
|
if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen {
|
|
unreadCount++
|
|
}
|
|
}
|
|
|
|
if unreadCount > 0 {
|
|
readID = notifications[0].ID
|
|
}
|
|
if len(notifications) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = "/notifications?max_id=" + pg.MaxID
|
|
}
|
|
|
|
cdata := s.cdata(c, "notifications", unreadCount,
|
|
c.s.Settings.NotificationInterval, "main")
|
|
data := &renderer.NotificationData{
|
|
Notifications: notifications,
|
|
UnreadCount: unreadCount,
|
|
ReadID: readID,
|
|
NextLink: nextLink,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.NotificationPage, data)
|
|
}
|
|
|
|
func (s *service) UserPage(c *client, id string, pageType string,
|
|
maxID string, minID string) (err error) {
|
|
|
|
var nextLink string
|
|
var statuses []*mastodon.Status
|
|
var users []*mastodon.Account
|
|
var pg = mastodon.Pagination{
|
|
MaxID: maxID,
|
|
MinID: minID,
|
|
Limit: 20,
|
|
}
|
|
|
|
user, err := c.GetAccount(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
isCurrent := c.s.UserID == user.ID
|
|
|
|
switch pageType {
|
|
case "":
|
|
statuses, err = c.GetAccountStatuses(c.ctx, id, false, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(statuses) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s?max_id=%s", id,
|
|
pg.MaxID)
|
|
}
|
|
case "following":
|
|
users, err = c.GetAccountFollowing(c.ctx, id, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(users) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s/following?max_id=%s",
|
|
id, pg.MaxID)
|
|
}
|
|
case "followers":
|
|
users, err = c.GetAccountFollowers(c.ctx, id, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(users) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s/followers?max_id=%s",
|
|
id, pg.MaxID)
|
|
}
|
|
case "media":
|
|
statuses, err = c.GetAccountStatuses(c.ctx, id, true, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(statuses) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s/media?max_id=%s",
|
|
id, pg.MaxID)
|
|
}
|
|
case "bookmarks":
|
|
if !isCurrent {
|
|
return errInvalidArgument
|
|
}
|
|
statuses, err = c.GetBookmarks(c.ctx, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(statuses) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s/bookmarks?max_id=%s",
|
|
id, pg.MaxID)
|
|
}
|
|
case "mutes":
|
|
if !isCurrent {
|
|
return errInvalidArgument
|
|
}
|
|
users, err = c.GetMutes(c.ctx, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(users) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s/mutes?max_id=%s",
|
|
id, pg.MaxID)
|
|
}
|
|
case "blocks":
|
|
if !isCurrent {
|
|
return errInvalidArgument
|
|
}
|
|
users, err = c.GetBlocks(c.ctx, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(users) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s/blocks?max_id=%s",
|
|
id, pg.MaxID)
|
|
}
|
|
case "likes":
|
|
if !isCurrent {
|
|
return errInvalidArgument
|
|
}
|
|
statuses, err = c.GetFavourites(c.ctx, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(statuses) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s/likes?max_id=%s",
|
|
id, pg.MaxID)
|
|
}
|
|
case "requests":
|
|
if !isCurrent {
|
|
return errInvalidArgument
|
|
}
|
|
users, err = c.GetFollowRequests(c.ctx, &pg)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(users) == 20 && len(pg.MaxID) > 0 {
|
|
nextLink = fmt.Sprintf("/user/%s/requests?max_id=%s",
|
|
id, pg.MaxID)
|
|
}
|
|
default:
|
|
return errInvalidArgument
|
|
}
|
|
|
|
for i := range statuses {
|
|
if statuses[i].Reblog != nil {
|
|
statuses[i].Reblog.RetweetedByID = statuses[i].ID
|
|
}
|
|
}
|
|
|
|
cdata := s.cdata(c, user.DisplayName+" @"+user.Acct, 0, 0, "")
|
|
data := &renderer.UserData{
|
|
User: user,
|
|
IsCurrent: isCurrent,
|
|
Type: pageType,
|
|
Users: users,
|
|
Statuses: statuses,
|
|
NextLink: nextLink,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.UserPage, data)
|
|
}
|
|
|
|
func (s *service) UserSearchPage(c *client,
|
|
id string, q string, offset int) (err error) {
|
|
|
|
var nextLink string
|
|
var title = "search"
|
|
|
|
user, err := c.GetAccount(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var results *mastodon.Results
|
|
if len(q) > 0 {
|
|
results, err = c.Search(c.ctx, q, "statuses", 20, true, offset, id, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
results = &mastodon.Results{}
|
|
}
|
|
|
|
if len(results.Statuses) == 20 {
|
|
offset += 20
|
|
nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id,
|
|
q, offset)
|
|
}
|
|
|
|
if len(q) > 0 {
|
|
title += " \"" + q + "\""
|
|
}
|
|
|
|
cdata := s.cdata(c, title, 0, 0, "")
|
|
data := &renderer.UserSearchData{
|
|
CommonData: cdata,
|
|
User: user,
|
|
Q: q,
|
|
Statuses: results.Statuses,
|
|
NextLink: nextLink,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.UserSearchPage, data)
|
|
}
|
|
|
|
func (s *service) MutePage(c *client, id string) (err error) {
|
|
user, err := c.GetAccount(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
cdata := s.cdata(c, "Mute "+user.DisplayName+" @"+user.Acct, 0, 0, "")
|
|
data := &renderer.UserData{
|
|
User: user,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.MutePage, data)
|
|
}
|
|
|
|
func (s *service) AboutPage(c *client) (err error) {
|
|
cdata := s.cdata(c, "about", 0, 0, "")
|
|
data := &renderer.AboutData{
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.AboutPage, data)
|
|
}
|
|
|
|
func (s *service) EmojiPage(c *client) (err error) {
|
|
emojis, err := c.GetInstanceEmojis(c.ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
cdata := s.cdata(c, "emojis", 0, 0, "")
|
|
data := &renderer.EmojiData{
|
|
Emojis: emojis,
|
|
CommonData: cdata,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.EmojiPage, data)
|
|
}
|
|
|
|
func (s *service) SearchPage(c *client,
|
|
q string, qType string, offset int) (err error) {
|
|
|
|
var nextLink string
|
|
var title = "search"
|
|
|
|
var results *mastodon.Results
|
|
if len(q) > 0 {
|
|
results, err = c.Search(c.ctx, q, qType, 20, true, offset, "", false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
results = &mastodon.Results{}
|
|
}
|
|
|
|
if (qType == "accounts" && len(results.Accounts) == 20) ||
|
|
(qType == "statuses" && len(results.Statuses) == 20) {
|
|
offset += 20
|
|
nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d",
|
|
q, qType, offset)
|
|
}
|
|
|
|
if len(q) > 0 {
|
|
title += " \"" + q + "\""
|
|
}
|
|
|
|
cdata := s.cdata(c, title, 0, 0, "")
|
|
data := &renderer.SearchData{
|
|
CommonData: cdata,
|
|
Q: q,
|
|
Type: qType,
|
|
Users: results.Accounts,
|
|
Statuses: results.Statuses,
|
|
NextLink: nextLink,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.SearchPage, data)
|
|
}
|
|
|
|
func (s *service) SettingsPage(c *client) (err error) {
|
|
cdata := s.cdata(c, "settings", 0, 0, "")
|
|
data := &renderer.SettingsData{
|
|
CommonData: cdata,
|
|
Settings: &c.s.Settings,
|
|
PostFormats: s.postFormats,
|
|
}
|
|
return s.renderer.Render(c.rctx, c.w, renderer.SettingsPage, data)
|
|
}
|
|
|
|
func (svc *service) FiltersPage(c *client) (err error) {
|
|
filters, err := c.GetFilters(c.ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
cdata := svc.cdata(c, "filters", 0, 0, "")
|
|
data := &renderer.FiltersData{
|
|
CommonData: cdata,
|
|
Filters: filters,
|
|
}
|
|
return svc.renderer.Render(c.rctx, c.w, renderer.FiltersPage, data)
|
|
}
|
|
|
|
func (svc *service) ProfilePage(c *client) (err error) {
|
|
u, err := c.GetAccountCurrentUser(c.ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// Some instances allow more than 4 fields, but make sure that there are
|
|
// at least 4 fields in the slice because the template depends on it.
|
|
if u.Source.Fields == nil {
|
|
u.Source.Fields = new([]mastodon.Field)
|
|
}
|
|
for len(*u.Source.Fields) < 4 {
|
|
*u.Source.Fields = append(*u.Source.Fields, mastodon.Field{})
|
|
}
|
|
cdata := svc.cdata(c, "edit profile", 0, 0, "")
|
|
data := &renderer.ProfileData{
|
|
CommonData: cdata,
|
|
User: u,
|
|
}
|
|
return svc.renderer.Render(c.rctx, c.w, renderer.ProfilePage, data)
|
|
}
|
|
|
|
func (s *service) ProfileUpdate(c *client, name, bio string, avatar, banner *multipart.FileHeader,
|
|
fields []mastodon.Field, locked bool) (err error) {
|
|
// Need to pass empty data to clear fields
|
|
if len(fields) == 0 {
|
|
fields = append(fields, mastodon.Field{})
|
|
}
|
|
p := &mastodon.Profile{
|
|
DisplayName: &name,
|
|
Note: &bio,
|
|
Avatar: avatar,
|
|
Header: banner,
|
|
Fields: &fields,
|
|
Locked: &locked,
|
|
}
|
|
_, err = c.AccountUpdate(c.ctx, p)
|
|
return err
|
|
}
|
|
|
|
func (s *service) ProfileDelAvatar(c *client) (err error) {
|
|
_, err = c.AccountDeleteAvatar(c.ctx)
|
|
return
|
|
}
|
|
|
|
func (s *service) ProfileDelBanner(c *client) (err error) {
|
|
_, err = c.AccountDeleteHeader(c.ctx)
|
|
return err
|
|
}
|
|
|
|
func (s *service) SingleInstance() (instance string, ok bool) {
|
|
if len(s.instance) > 0 {
|
|
instance = s.instance
|
|
ok = true
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *service) NewSession(c *client, instance string) (rurl string, sess *model.Session, err error) {
|
|
var instanceURL string
|
|
if strings.HasPrefix(instance, "https://") {
|
|
instanceURL = instance
|
|
instance = strings.TrimPrefix(instance, "https://")
|
|
} else {
|
|
instanceURL = "https://" + instance
|
|
}
|
|
|
|
csrf, err := util.NewCSRFToken()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
app, err := mastodon.RegisterApp(c.ctx, &mastodon.AppConfig{
|
|
Server: instanceURL,
|
|
ClientName: s.cname,
|
|
Scopes: s.cscope,
|
|
Website: s.cwebsite,
|
|
RedirectURIs: s.cwebsite + "/oauth_callback",
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
rurl = app.AuthURI
|
|
sess = &model.Session{
|
|
Instance: instance,
|
|
ClientID: app.ClientID,
|
|
ClientSecret: app.ClientSecret,
|
|
CSRFToken: csrf,
|
|
Settings: *model.NewSettings(),
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *service) Signin(c *client, code string) (err error) {
|
|
if len(code) < 1 {
|
|
err = errInvalidArgument
|
|
return
|
|
}
|
|
err = c.AuthenticateToken(c.ctx, code, s.cwebsite+"/oauth_callback")
|
|
if err != nil {
|
|
return
|
|
}
|
|
u, err := c.GetAccountCurrentUser(c.ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
c.s.AccessToken = c.GetAccessToken(c.ctx)
|
|
c.s.UserID = u.ID
|
|
return c.setSession(c.s)
|
|
}
|
|
|
|
func (s *service) Signout(c *client) (err error) {
|
|
return c.RevokeToken(c.ctx)
|
|
}
|
|
|
|
func (s *service) Post(c *client, content string, replyToID string,
|
|
format string, visibility string, isNSFW bool,
|
|
files []*multipart.FileHeader) (id string, err error) {
|
|
|
|
var mediaIDs []string
|
|
for _, f := range files {
|
|
a, err := c.UploadMediaFromMultipartFileHeader(c.ctx, f)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
mediaIDs = append(mediaIDs, a.ID)
|
|
}
|
|
|
|
tweet := &mastodon.Toot{
|
|
Status: content,
|
|
InReplyToID: replyToID,
|
|
MediaIDs: mediaIDs,
|
|
ContentType: format,
|
|
Visibility: visibility,
|
|
Sensitive: isNSFW,
|
|
}
|
|
st, err := c.PostStatus(c.ctx, tweet)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return st.ID, nil
|
|
}
|
|
|
|
func (s *service) Like(c *client, id string) (count int64, err error) {
|
|
st, err := c.Favourite(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
count = st.FavouritesCount
|
|
return
|
|
}
|
|
|
|
func (s *service) UnLike(c *client, id string) (count int64, err error) {
|
|
st, err := c.Unfavourite(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
count = st.FavouritesCount
|
|
return
|
|
}
|
|
|
|
func (s *service) Retweet(c *client, id string) (count int64, err error) {
|
|
st, err := c.Reblog(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if st.Reblog != nil {
|
|
count = st.Reblog.ReblogsCount
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *service) UnRetweet(c *client, id string) (
|
|
count int64, err error) {
|
|
st, err := c.Unreblog(c.ctx, id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
count = st.ReblogsCount
|
|
return
|
|
}
|
|
|
|
func (s *service) Vote(c *client, id string, choices []string) (err error) {
|
|
_, err = c.Vote(c.ctx, id, choices)
|
|
return
|
|
}
|
|
|
|
func (s *service) Follow(c *client, id string, reblogs *bool) (err error) {
|
|
_, err = c.AccountFollow(c.ctx, id, reblogs)
|
|
return
|
|
}
|
|
|
|
func (s *service) UnFollow(c *client, id string) (err error) {
|
|
_, err = c.AccountUnfollow(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) Accept(c *client, id string) (err error) {
|
|
return c.FollowRequestAuthorize(c.ctx, id)
|
|
}
|
|
|
|
func (s *service) Reject(c *client, id string) (err error) {
|
|
return c.FollowRequestReject(c.ctx, id)
|
|
}
|
|
|
|
func (s *service) Mute(c *client, id string, notifications bool, duration int) (err error) {
|
|
_, err = c.AccountMute(c.ctx, id, notifications, duration)
|
|
return
|
|
}
|
|
|
|
func (s *service) UnMute(c *client, id string) (err error) {
|
|
_, err = c.AccountUnmute(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) Block(c *client, id string) (err error) {
|
|
_, err = c.AccountBlock(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) UnBlock(c *client, id string) (err error) {
|
|
_, err = c.AccountUnblock(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) Subscribe(c *client, id string) (err error) {
|
|
_, err = c.Subscribe(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) UnSubscribe(c *client, id string) (err error) {
|
|
_, err = c.UnSubscribe(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) SaveSettings(c *client, settings *model.Settings) (err error) {
|
|
switch settings.NotificationInterval {
|
|
case 0, 30, 60, 120, 300, 600:
|
|
default:
|
|
return errInvalidArgument
|
|
}
|
|
if len(settings.CSS) > 0 {
|
|
if len(settings.CSS) > 1<<20 {
|
|
return errInvalidArgument
|
|
}
|
|
// For some reason, browsers convert CRLF to LF before calculating
|
|
// the hash of the inline resources.
|
|
settings.CSS = strings.ReplaceAll(settings.CSS, "\x0d\x0a", "\x0a")
|
|
|
|
h := sha256.Sum256([]byte(settings.CSS))
|
|
settings.CSSHash = base64.StdEncoding.EncodeToString(h[:])
|
|
} else {
|
|
settings.CSSHash = ""
|
|
}
|
|
c.s.Settings = *settings
|
|
return c.setSession(c.s)
|
|
}
|
|
|
|
func (s *service) MuteConversation(c *client, id string) (err error) {
|
|
_, err = c.MuteConversation(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) UnMuteConversation(c *client, id string) (err error) {
|
|
_, err = c.UnmuteConversation(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) Delete(c *client, id string) (err error) {
|
|
return c.DeleteStatus(c.ctx, id)
|
|
}
|
|
|
|
func (s *service) ReadNotifications(c *client, maxID string) (err error) {
|
|
return c.ReadNotifications(c.ctx, maxID)
|
|
}
|
|
|
|
func (s *service) Bookmark(c *client, id string) (err error) {
|
|
_, err = c.Bookmark(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (s *service) UnBookmark(c *client, id string) (err error) {
|
|
_, err = c.Unbookmark(c.ctx, id)
|
|
return
|
|
}
|
|
|
|
func (svc *service) Filter(c *client, phrase string, wholeWord bool) (err error) {
|
|
fctx := []string{"home", "notifications", "public", "thread"}
|
|
return c.AddFilter(c.ctx, phrase, fctx, true, wholeWord, nil)
|
|
}
|
|
|
|
func (svc *service) UnFilter(c *client, id string) (err error) {
|
|
return c.RemoveFilter(c.ctx, id)
|
|
}
|