diff options
| author | r <r@freesoftwareextremist.com> | 2021-01-30 04:13:22 +0000 | 
|---|---|---|
| committer | r <r@freesoftwareextremist.com> | 2021-01-30 04:13:22 +0000 | 
| commit | 9f34b607498c09b4a21bdcc82b3295f6c94bd058 (patch) | |
| tree | c5a528cad05227f15f30b07f6d0056045176166b | |
| parent | c3f39210d8451d65f3ecaa88046380f42dc536a1 (diff) | |
| parent | 3ac95ab3b117ee8867a30c8e4b30ab37411e5ccf (diff) | |
| download | bloat-9f34b607498c09b4a21bdcc82b3295f6c94bd058.tar.gz bloat-9f34b607498c09b4a21bdcc82b3295f6c94bd058.zip | |
Merge branch 'master' into absolute_fluoride
| -rw-r--r-- | Makefile | 1 | ||||
| -rw-r--r-- | config/config.go | 2 | ||||
| -rw-r--r-- | main.go | 9 | ||||
| -rw-r--r-- | mastodon/accounts.go | 2 | ||||
| -rw-r--r-- | mastodon/mastodon.go | 7 | ||||
| -rw-r--r-- | mastodon/status.go | 6 | ||||
| -rw-r--r-- | model/client.go | 19 | ||||
| -rw-r--r-- | model/post.go | 1 | ||||
| -rw-r--r-- | model/settings.go | 40 | ||||
| -rw-r--r-- | renderer/model.go | 15 | ||||
| -rw-r--r-- | repo/appRepo.go | 6 | ||||
| -rw-r--r-- | repo/sessionRepo.go | 6 | ||||
| -rw-r--r-- | service/auth.go | 488 | ||||
| -rw-r--r-- | service/logging.go | 349 | ||||
| -rw-r--r-- | service/service.go | 393 | ||||
| -rw-r--r-- | service/transport.go | 1080 | ||||
| -rw-r--r-- | static/fluoride.js | 15 | ||||
| -rw-r--r-- | static/style.css | 37 | ||||
| -rw-r--r-- | templates/header.tmpl | 4 | ||||
| -rw-r--r-- | templates/nav.tmpl | 10 | ||||
| -rw-r--r-- | templates/notification.tmpl | 50 | ||||
| -rw-r--r-- | templates/postform.tmpl | 2 | ||||
| -rw-r--r-- | templates/requestlist.tmpl | 36 | ||||
| -rw-r--r-- | templates/root.tmpl | 2 | ||||
| -rw-r--r-- | templates/settings.tmpl | 16 | ||||
| -rw-r--r-- | templates/status.tmpl | 32 | ||||
| -rw-r--r-- | templates/timeline.tmpl | 10 | ||||
| -rw-r--r-- | templates/user.tmpl | 26 | ||||
| -rw-r--r-- | util/kv.go (renamed from kv/kv.go) | 2 | 
29 files changed, 879 insertions, 1787 deletions
| @@ -7,7 +7,6 @@ SHAREPATH=$(PREFIX)/share/bloat  TMPL=templates/*.tmpl  SRC=main.go		\  	config/*.go 	\ -	kv/*.go 	\  	mastodon/*.go	\  	model/*.go	\  	renderer/*.go 	\ diff --git a/config/config.go b/config/config.go index d6140e5..8678f52 100644 --- a/config/config.go +++ b/config/config.go @@ -101,7 +101,7 @@ func Parse(r io.Reader) (c *config, err error) {  		case "log_file":  			c.LogFile = val  		default: -			return nil, errors.New("invliad config key " + key) +			return nil, errors.New("invalid config key " + key)  		}  	} @@ -12,7 +12,6 @@ import (  	"time"  	"bloat/config" -	"bloat/kv"  	"bloat/renderer"  	"bloat/repo"  	"bloat/service" @@ -76,13 +75,13 @@ func main() {  	}  	sessionDBPath := filepath.Join(config.DatabasePath, "session") -	sessionDB, err := kv.NewDatabse(sessionDBPath) +	sessionDB, err := util.NewDatabse(sessionDBPath)  	if err != nil {  		errExit(err)  	}  	appDBPath := filepath.Join(config.DatabasePath, "app") -	appDB, err := kv.NewDatabse(appDBPath) +	appDB, err := util.NewDatabse(appDBPath)  	if err != nil {  		errExit(err)  	} @@ -114,9 +113,7 @@ func main() {  	s := service.NewService(config.ClientName, config.ClientScope,  		config.ClientWebsite, customCSS, config.PostFormats, renderer,  		sessionRepo, appRepo, config.SingleInstance) -	s = service.NewAuthService(sessionRepo, appRepo, s) -	s = service.NewLoggingService(logger, s) -	handler := service.NewHandler(s, config.StaticDirectory) +	handler := service.NewHandler(s, logger, config.StaticDirectory)  	logger.Println("listening on", config.ListenAddress)  	err = http.ListenAndServe(config.ListenAddress, handler) diff --git a/mastodon/accounts.go b/mastodon/accounts.go index c5eb227..694e672 100644 --- a/mastodon/accounts.go +++ b/mastodon/accounts.go @@ -60,7 +60,7 @@ func (c *Client) GetAccount(ctx context.Context, id string) (*Account, error) {  	if err != nil {  		return nil, err  	} -	if account.Pleroma == nil { +	if account.Pleroma == nil || len(account.Pleroma.Relationship.ID) < 1 {  		rs, err := c.GetAccountRelationships(ctx, []string{id})  		if err != nil {  			return nil, err diff --git a/mastodon/mastodon.go b/mastodon/mastodon.go index 658b09b..8678314 100644 --- a/mastodon/mastodon.go +++ b/mastodon/mastodon.go @@ -91,7 +91,12 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in  		var buf bytes.Buffer  		mw := multipart.NewWriter(&buf) -		part, err := mw.CreateFormFile("file", filepath.Base(file.Filename)) +		fname := filepath.Base(file.Filename) +		err = mw.WriteField("description", fname) +		if err != nil { +			return err +		} +		part, err := mw.CreateFormFile("file", fname)  		if err != nil {  			return err  		} diff --git a/mastodon/status.go b/mastodon/status.go index c8555d6..80e7e0e 100644 --- a/mastodon/status.go +++ b/mastodon/status.go @@ -191,9 +191,11 @@ func (c *Client) GetTimelineHome(ctx context.Context, pg *Pagination) ([]*Status  }  // GetTimelinePublic return statuses from public timeline. -func (c *Client) GetTimelinePublic(ctx context.Context, isLocal bool, pg *Pagination) ([]*Status, error) { +func (c *Client) GetTimelinePublic(ctx context.Context, isLocal bool, instance string, pg *Pagination) ([]*Status, error) {  	params := url.Values{} -	if isLocal { +	if len(instance) > 0 { +		params.Set("instance", instance) +	} else if isLocal {  		params.Set("local", "true")  	} diff --git a/model/client.go b/model/client.go deleted file mode 100644 index 931ddaa..0000000 --- a/model/client.go +++ /dev/null @@ -1,19 +0,0 @@ -package model - -import ( -	"io" - -	"bloat/mastodon" -) - -type ClientCtx struct { -	SessionID string -	CSRFToken string -} - -type Client struct { -	*mastodon.Client -	Writer  io.Writer -	Ctx     ClientCtx -	Session Session -} diff --git a/model/post.go b/model/post.go index 831f74f..40118ed 100644 --- a/model/post.go +++ b/model/post.go @@ -10,7 +10,6 @@ type PostContext struct {  	DefaultFormat     string  	ReplyContext      *ReplyContext  	Formats           []PostFormat -	DarkMode          bool  }  type ReplyContext struct { diff --git a/model/settings.go b/model/settings.go index 337a6a3..6d17901 100644 --- a/model/settings.go +++ b/model/settings.go @@ -1,29 +1,29 @@  package model  type Settings struct { -	DefaultVisibility        string `json:"default_visibility"` -	DefaultFormat            string `json:"default_format"` -	CopyScope                bool   `json:"copy_scope"` -	ThreadInNewTab           bool   `json:"thread_in_new_tab"` -	HideAttachments          bool   `json:"hide_attachments"` -	MaskNSFW                 bool   `json:"mask_nfsw"` -	AutoRefreshNotifications bool   `json:"auto_refresh_notifications"` -	FluorideMode             bool   `json:"fluoride_mode"` -	DarkMode                 bool   `json:"dark_mode"` -	AntiDopamineMode         bool   `json:"anti_dopamine_mode"` +	DefaultVisibility    string `json:"default_visibility"` +	DefaultFormat        string `json:"default_format"` +	CopyScope            bool   `json:"copy_scope"` +	ThreadInNewTab       bool   `json:"thread_in_new_tab"` +	HideAttachments      bool   `json:"hide_attachments"` +	MaskNSFW             bool   `json:"mask_nfsw"` +	NotificationInterval int    `json:"notifications_interval"` +	FluorideMode         bool   `json:"fluoride_mode"` +	DarkMode             bool   `json:"dark_mode"` +	AntiDopamineMode     bool   `json:"anti_dopamine_mode"`  }  func NewSettings() *Settings {  	return &Settings{ -		DefaultVisibility:        "public", -		DefaultFormat:            "", -		CopyScope:                true, -		ThreadInNewTab:           false, -		HideAttachments:          false, -		MaskNSFW:                 true, -		AutoRefreshNotifications: false, -		FluorideMode:             false, -		DarkMode:                 false, -		AntiDopamineMode:         false, +		DefaultVisibility:    "public", +		DefaultFormat:        "", +		CopyScope:            true, +		ThreadInNewTab:       false, +		HideAttachments:      false, +		MaskNSFW:             true, +		NotificationInterval: 0, +		FluorideMode:         false, +		DarkMode:             false, +		AntiDopamineMode:     false,  	}  } diff --git a/renderer/model.go b/renderer/model.go index 0505c0b..93cbbc6 100644 --- a/renderer/model.go +++ b/renderer/model.go @@ -14,6 +14,7 @@ type Context struct {  	CSRFToken        string  	UserID           string  	AntiDopamineMode bool +	Referrer         string  }  type NavData struct { @@ -23,12 +24,12 @@ type NavData struct {  }  type CommonData struct { -	Title       string -	CustomCSS   string -	CSRFToken   string -	Count       int -	AutoRefresh bool -	Target      string +	Title           string +	CustomCSS       string +	CSRFToken       string +	Count           int +	RefreshInterval int +	Target          string  }  type ErrorData struct { @@ -51,6 +52,8 @@ type RootData struct {  type TimelineData struct {  	*CommonData  	Title    string +	Type     string +	Instance string  	Statuses []*mastodon.Status  	NextLink string  	PrevLink string diff --git a/repo/appRepo.go b/repo/appRepo.go index 6338c4a..d97ac1f 100644 --- a/repo/appRepo.go +++ b/repo/appRepo.go @@ -3,15 +3,15 @@ package repo  import (  	"encoding/json" -	"bloat/kv" +	"bloat/util"  	"bloat/model"  )  type appRepo struct { -	db *kv.Database +	db *util.Database  } -func NewAppRepo(db *kv.Database) *appRepo { +func NewAppRepo(db *util.Database) *appRepo {  	return &appRepo{  		db: db,  	} diff --git a/repo/sessionRepo.go b/repo/sessionRepo.go index 15e3d31..2097c3e 100644 --- a/repo/sessionRepo.go +++ b/repo/sessionRepo.go @@ -3,15 +3,15 @@ package repo  import (  	"encoding/json" -	"bloat/kv" +	"bloat/util"  	"bloat/model"  )  type sessionRepo struct { -	db *kv.Database +	db *util.Database  } -func NewSessionRepo(db *kv.Database) *sessionRepo { +func NewSessionRepo(db *util.Database) *sessionRepo {  	return &sessionRepo{  		db: db,  	} diff --git a/service/auth.go b/service/auth.go deleted file mode 100644 index c670130..0000000 --- a/service/auth.go +++ /dev/null @@ -1,488 +0,0 @@ -package service - -import ( -	"errors" -	"mime/multipart" - -	"bloat/mastodon" -	"bloat/model" -) - -var ( -	errInvalidSession   = errors.New("invalid session") -	errInvalidAccessToken = errors.New("invalid access token") -	errInvalidCSRFToken = errors.New("invalid csrf token") -) - -type as struct { -	sessionRepo model.SessionRepo -	appRepo     model.AppRepo -	Service -} - -func NewAuthService(sessionRepo model.SessionRepo, appRepo model.AppRepo, s Service) Service { -	return &as{sessionRepo, appRepo, s} -} - -func (s *as) initClient(c *model.Client) (err error) { -	if len(c.Ctx.SessionID) < 1 { -		return errInvalidSession -	} -	session, err := s.sessionRepo.Get(c.Ctx.SessionID) -	if err != nil { -		return errInvalidSession -	} -	app, err := s.appRepo.Get(session.InstanceDomain) -	if err != nil { -		return -	} -	mc := mastodon.NewClient(&mastodon.Config{ -		Server:       app.InstanceURL, -		ClientID:     app.ClientID, -		ClientSecret: app.ClientSecret, -		AccessToken:  session.AccessToken, -	}) -	c.Client = mc -	c.Session = session -	return nil -} - -func (s *as) authenticateClient(c *model.Client) (err error) { -	err = s.initClient(c) -	if err != nil { -		return -	} -	if len(c.Session.AccessToken) < 1 { -		return errInvalidAccessToken -	} -	return nil -} - -func checkCSRF(c *model.Client) (err error) { -	if c.Ctx.CSRFToken != c.Session.CSRFToken { -		return errInvalidCSRFToken -	} -	return nil -} - -func (s *as) ServeErrorPage(c *model.Client, err error) { -	s.authenticateClient(c) -	s.Service.ServeErrorPage(c, err) -} - -func (s *as) ServeSigninPage(c *model.Client) (err error) { -	return s.Service.ServeSigninPage(c) -} - -func (s *as) ServeRootPage(c *model.Client) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeRootPage(c) -} - -func (s *as) ServeNavPage(c *model.Client) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeNavPage(c) -} - -func (s *as) ServeTimelinePage(c *model.Client, tType string, -	maxID string, minID string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeTimelinePage(c, tType, maxID, minID) -} - -func (s *as) ServeThreadPage(c *model.Client, id string, reply bool) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeThreadPage(c, id, reply) -} - -func (s *as) ServeStatusPopup(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeStatusPopup(c, id) -} - -func (s *as) ServeLikedByPage(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeLikedByPage(c, id) -} - -func (s *as) ServeRetweetedByPage(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeRetweetedByPage(c, id) -} - -func (s *as) ServeNotificationPage(c *model.Client, -	maxID string, minID string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeNotificationPage(c, maxID, minID) -} - -func (s *as) ServeUserPage(c *model.Client, id string, -	pageType string, maxID string, minID string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeUserPage(c, id, pageType, maxID, minID) -} - -func (s *as) ServeAboutPage(c *model.Client) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeAboutPage(c) -} - -func (s *as) ServeEmojiPage(c *model.Client) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeEmojiPage(c) -} - -func (s *as) ServeSearchPage(c *model.Client, q string, -	qType string, offset int) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeSearchPage(c, q, qType, offset) -} - -func (s *as) ServeUserSearchPage(c *model.Client, -	id string, q string, offset int) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeUserSearchPage(c, id, q, offset) -} - -func (s *as) ServeSettingsPage(c *model.Client) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	return s.Service.ServeSettingsPage(c) -} - -func (s *as) NewSession(instance string) (redirectUrl string, -	sessionID string, err error) { -	return s.Service.NewSession(instance) -} - -func (s *as) Signin(c *model.Client, sessionID string, -	code string) (token string, userID string, err error) { -	err = s.authenticateClient(c) -	if err != nil && err != errInvalidAccessToken { -		return -	} - -	token, userID, err = s.Service.Signin(c, c.Session.ID, code) -	if err != nil { -		return -	} - -	c.Session.AccessToken = token -	c.Session.UserID = userID - -	err = s.sessionRepo.Add(c.Session) -	if err != nil { -		return -	} - -	return -} - -func (s *as) Signout(c *model.Client) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	s.Service.Signout(c) -	return -} - -func (s *as) Post(c *model.Client, content string, -	replyToID string, format string, visibility string, isNSFW bool, -	files []*multipart.FileHeader) (id string, err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Post(c, content, replyToID, format, visibility, isNSFW, files) -} - -func (s *as) Like(c *model.Client, id string) (count int64, err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Like(c, id) -} - -func (s *as) UnLike(c *model.Client, id string) (count int64, err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.UnLike(c, id) -} - -func (s *as) Retweet(c *model.Client, id string) (count int64, err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Retweet(c, id) -} - -func (s *as) UnRetweet(c *model.Client, id string) (count int64, err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.UnRetweet(c, id) -} - -func (s *as) Vote(c *model.Client, id string, -	choices []string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Vote(c, id, choices) -} - -func (s *as) Follow(c *model.Client, id string, reblogs *bool) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Follow(c, id, reblogs) -} - -func (s *as) UnFollow(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.UnFollow(c, id) -} - -func (s *as) Mute(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Mute(c, id) -} - -func (s *as) UnMute(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.UnMute(c, id) -} - -func (s *as) Block(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Block(c, id) -} - -func (s *as) UnBlock(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.UnBlock(c, id) -} - -func (s *as) Subscribe(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Subscribe(c, id) -} - -func (s *as) UnSubscribe(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.UnSubscribe(c, id) -} - -func (s *as) SaveSettings(c *model.Client, settings *model.Settings) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.SaveSettings(c, settings) -} - -func (s *as) MuteConversation(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.MuteConversation(c, id) -} - -func (s *as) UnMuteConversation(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.UnMuteConversation(c, id) -} - -func (s *as) Delete(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Delete(c, id) -} - -func (s *as) ReadNotifications(c *model.Client, maxID string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.ReadNotifications(c, maxID) -} - -func (s *as) Bookmark(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.Bookmark(c, id) -} - -func (s *as) UnBookmark(c *model.Client, id string) (err error) { -	err = s.authenticateClient(c) -	if err != nil { -		return -	} -	err = checkCSRF(c) -	if err != nil { -		return -	} -	return s.Service.UnBookmark(c, id) -} diff --git a/service/logging.go b/service/logging.go deleted file mode 100644 index d283a55..0000000 --- a/service/logging.go +++ /dev/null @@ -1,349 +0,0 @@ -package service - -import ( -	"log" -	"mime/multipart" -	"time" - -	"bloat/model" -) - -type ls struct { -	logger *log.Logger -	Service -} - -func NewLoggingService(logger *log.Logger, s Service) Service { -	return &ls{logger, s} -} - -func (s *ls) ServeErrorPage(c *model.Client, err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, err=%v, took=%v\n", -			"ServeErrorPage", err, time.Since(begin)) -	}(time.Now()) -	s.Service.ServeErrorPage(c, err) -} - -func (s *ls) ServeSigninPage(c *model.Client) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeSigninPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeSigninPage(c) -} - -func (s *ls) ServeRootPage(c *model.Client) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeRootPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeRootPage(c) -} - -func (s *ls) ServeNavPage(c *model.Client) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeNavPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeNavPage(c) -} - -func (s *ls) ServeTimelinePage(c *model.Client, tType string, -	maxID string, minID string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, type=%v, took=%v, err=%v\n", -			"ServeTimelinePage", tType, time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeTimelinePage(c, tType, maxID, minID) -} - -func (s *ls) ServeThreadPage(c *model.Client, id string, -	reply bool) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"ServeThreadPage", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeThreadPage(c, id, reply) -} - -func (s *ls) ServeStatusPopup(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"ServeStatusPopup", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeStatusPopup(c, id) -} - -func (s *ls) ServeLikedByPage(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"ServeLikedByPage", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeLikedByPage(c, id) -} - -func (s *ls) ServeRetweetedByPage(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"ServeRetweetedByPage", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeRetweetedByPage(c, id) -} - -func (s *ls) ServeNotificationPage(c *model.Client, -	maxID string, minID string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeNotificationPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeNotificationPage(c, maxID, minID) -} - -func (s *ls) ServeUserPage(c *model.Client, id string, -	pageType string, maxID string, minID string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, type=%v, took=%v, err=%v\n", -			"ServeUserPage", id, pageType, time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeUserPage(c, id, pageType, maxID, minID) -} - -func (s *ls) ServeAboutPage(c *model.Client) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeAboutPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeAboutPage(c) -} - -func (s *ls) ServeEmojiPage(c *model.Client) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeEmojiPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeEmojiPage(c) -} - -func (s *ls) ServeSearchPage(c *model.Client, q string, -	qType string, offset int) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeSearchPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeSearchPage(c, q, qType, offset) -} - -func (s *ls) ServeUserSearchPage(c *model.Client, -	id string, q string, offset int) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeUserSearchPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeUserSearchPage(c, id, q, offset) -} - -func (s *ls) ServeSettingsPage(c *model.Client) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"ServeSettingsPage", time.Since(begin), err) -	}(time.Now()) -	return s.Service.ServeSettingsPage(c) -} - -func (s *ls) NewSession(instance string) (redirectUrl string, -	sessionID string, err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, instance=%v, took=%v, err=%v\n", -			"NewSession", instance, time.Since(begin), err) -	}(time.Now()) -	return s.Service.NewSession(instance) -} - -func (s *ls) Signin(c *model.Client, sessionID string, -	code string) (token string, userID string, err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, session_id=%v, took=%v, err=%v\n", -			"Signin", sessionID, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Signin(c, sessionID, code) -} - -func (s *ls) Signout(c *model.Client) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"Signout", time.Since(begin), err) -	}(time.Now()) -	return s.Service.Signout(c) -} - -func (s *ls) Post(c *model.Client, content string, -	replyToID string, format string, visibility string, isNSFW bool, -	files []*multipart.FileHeader) (id string, err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"Post", time.Since(begin), err) -	}(time.Now()) -	return s.Service.Post(c, content, replyToID, format, -		visibility, isNSFW, files) -} - -func (s *ls) Like(c *model.Client, id string) (count int64, err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Like", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Like(c, id) -} - -func (s *ls) UnLike(c *model.Client, id string) (count int64, err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"UnLike", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.UnLike(c, id) -} - -func (s *ls) Retweet(c *model.Client, id string) (count int64, err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Retweet", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Retweet(c, id) -} - -func (s *ls) UnRetweet(c *model.Client, id string) (count int64, err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"UnRetweet", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.UnRetweet(c, id) -} - -func (s *ls) Vote(c *model.Client, id string, choices []string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Vote", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Vote(c, id, choices) -} - -func (s *ls) Follow(c *model.Client, id string, reblogs *bool) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Follow", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Follow(c, id, reblogs) -} - -func (s *ls) UnFollow(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"UnFollow", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.UnFollow(c, id) -} - -func (s *ls) Mute(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Mute", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Mute(c, id) -} - -func (s *ls) UnMute(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"UnMute", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.UnMute(c, id) -} - -func (s *ls) Block(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Block", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Block(c, id) -} - -func (s *ls) UnBlock(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"UnBlock", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.UnBlock(c, id) -} - -func (s *ls) Subscribe(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Subscribe", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Subscribe(c, id) -} - -func (s *ls) UnSubscribe(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"UnSubscribe", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.UnSubscribe(c, id) -} - -func (s *ls) SaveSettings(c *model.Client, settings *model.Settings) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, took=%v, err=%v\n", -			"SaveSettings", time.Since(begin), err) -	}(time.Now()) -	return s.Service.SaveSettings(c, settings) -} - -func (s *ls) MuteConversation(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"MuteConversation", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.MuteConversation(c, id) -} - -func (s *ls) UnMuteConversation(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"UnMuteConversation", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.UnMuteConversation(c, id) -} - -func (s *ls) Delete(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Delete", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Delete(c, id) -} - -func (s *ls) ReadNotifications(c *model.Client, maxID string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, max_id=%v, took=%v, err=%v\n", -			"ReadNotifications", maxID, time.Since(begin), err) -	}(time.Now()) -	return s.Service.ReadNotifications(c, maxID) -} - -func (s *ls) Bookmark(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"Bookmark", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.Bookmark(c, id) -} - -func (s *ls) UnBookmark(c *model.Client, id string) (err error) { -	defer func(begin time.Time) { -		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", -			"UnBookmark", id, time.Since(begin), err) -	}(time.Now()) -	return s.Service.UnBookmark(c, id) -} diff --git a/service/service.go b/service/service.go index 82ff972..1666959 100644 --- a/service/service.go +++ b/service/service.go @@ -20,54 +20,6 @@ var (  	errInvalidArgument = errors.New("invalid argument")  ) -type Service interface { -	ServeErrorPage(c *model.Client, err error) -	ServeSigninPage(c *model.Client) (err error) -	ServeRootPage(c *model.Client) (err error) -	ServeNavPage(c *model.Client) (err error) -	ServeTimelinePage(c *model.Client, tType string, maxID string, -		minID string) (err error) -	ServeThreadPage(c *model.Client, id string, reply bool) (err error) -	ServeStatusPopup(c *model.Client, id string) (err error) -	ServeLikedByPage(c *model.Client, id string) (err error) -	ServeRetweetedByPage(c *model.Client, id string) (err error) -	ServeNotificationPage(c *model.Client, maxID string, minID string) (err error) -	ServeUserPage(c *model.Client, id string, pageType string, maxID string, -		minID string) (err error) -	ServeAboutPage(c *model.Client) (err error) -	ServeEmojiPage(c *model.Client) (err error) -	ServeSearchPage(c *model.Client, q string, qType string, offset int) (err error) -	ServeUserSearchPage(c *model.Client, id string, q string, offset int) (err error) -	ServeSettingsPage(c *model.Client) (err error) -	SingleInstance() (instance string, ok bool) -	NewSession(instance string) (redirectUrl string, sessionID string, err error) -	Signin(c *model.Client, sessionID string, code string) (token string, -		userID string, err error) -	Signout(c *model.Client) (err error) -	Post(c *model.Client, content string, replyToID string, format string, visibility string, -		isNSFW bool, files []*multipart.FileHeader) (id string, err error) -	Like(c *model.Client, id string) (count int64, err error) -	UnLike(c *model.Client, id string) (count int64, err error) -	Retweet(c *model.Client, id string) (count int64, err error) -	UnRetweet(c *model.Client, id string) (count int64, err error) -	Vote(c *model.Client, id string, choices []string) (err error) -	Follow(c *model.Client, id string, reblogs *bool) (err error) -	UnFollow(c *model.Client, id string) (err error) -	Mute(c *model.Client, id string) (err error) -	UnMute(c *model.Client, id string) (err error) -	Block(c *model.Client, id string) (err error) -	UnBlock(c *model.Client, id string) (err error) -	Subscribe(c *model.Client, id string) (err error) -	UnSubscribe(c *model.Client, id string) (err error) -	SaveSettings(c *model.Client, settings *model.Settings) (err error) -	MuteConversation(c *model.Client, id string) (err error) -	UnMuteConversation(c *model.Client, id string) (err error) -	Delete(c *model.Client, id string) (err error) -	ReadNotifications(c *model.Client, maxID string) (err error) -	Bookmark(c *model.Client, id string) (err error) -	UnBookmark(c *model.Client, id string) (err error) -} -  type service struct {  	clientName     string  	clientScope    string @@ -89,7 +41,7 @@ func NewService(clientName string,  	sessionRepo model.SessionRepo,  	appRepo model.AppRepo,  	singleInstance string, -) Service { +) *service {  	return &service{  		clientName:     clientName,  		clientScope:    clientScope, @@ -103,12 +55,14 @@ func NewService(clientName string,  	}  } -func getRendererContext(c *model.Client) *renderer.Context { +func getRendererContext(c *client) *renderer.Context {  	var settings model.Settings  	var session model.Session +	var referrer string  	if c != nil {  		settings = c.Session.Settings  		session = c.Session +		referrer = c.url()  	} else {  		settings = *model.NewSettings()  	} @@ -121,6 +75,7 @@ func getRendererContext(c *model.Client) *renderer.Context {  		CSRFToken:        session.CSRFToken,  		UserID:           session.UserID,  		AntiDopamineMode: settings.AntiDopamineMode, +		Referrer:         referrer,  	}  } @@ -129,26 +84,21 @@ func addToReplyMap(m map[string][]mastodon.ReplyInfo, key interface{},  	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 (svc *service) getCommonData(c *model.Client, -	title string) (data *renderer.CommonData) { - +func (s *service) getCommonData(c *client, title string) (data *renderer.CommonData) {  	data = &renderer.CommonData{ -		Title:     title + " - " + svc.clientName, -		CustomCSS: svc.customCSS, +		Title:     title + " - " + s.clientName, +		CustomCSS: s.customCSS,  	}  	if c != nil && c.Session.IsLoggedIn() {  		data.CSRFToken = c.Session.CSRFToken @@ -156,66 +106,59 @@ func (svc *service) getCommonData(c *model.Client,  	return  } -func (svc *service) ServeErrorPage(c *model.Client, err error) { +func (s *service) ErrorPage(c *client, err error) {  	var errStr string  	if err != nil {  		errStr = err.Error()  	} - -	commonData := svc.getCommonData(nil, "error") +	commonData := s.getCommonData(nil, "error")  	data := &renderer.ErrorData{  		CommonData: commonData,  		Error:      errStr,  	} -  	rCtx := getRendererContext(c) -	svc.renderer.Render(rCtx, c.Writer, renderer.ErrorPage, data) +	s.renderer.Render(rCtx, c, renderer.ErrorPage, data)  } -func (svc *service) ServeSigninPage(c *model.Client) (err error) { -	commonData := svc.getCommonData(nil, "signin") +func (s *service) SigninPage(c *client) (err error) { +	commonData := s.getCommonData(nil, "signin")  	data := &renderer.SigninData{  		CommonData: commonData,  	} -  	rCtx := getRendererContext(nil) -	return svc.renderer.Render(rCtx, c.Writer, renderer.SigninPage, data) +	return s.renderer.Render(rCtx, c, renderer.SigninPage, data)  } -func (svc *service) ServeRootPage(c *model.Client) (err error) { +func (s *service) RootPage(c *client) (err error) {  	data := &renderer.RootData{ -		Title: svc.clientName, +		Title: s.clientName,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.RootPage, data) +	return s.renderer.Render(rCtx, c, renderer.RootPage, data)  } -func (svc *service) ServeNavPage(c *model.Client) (err error) { +func (s *service) NavPage(c *client) (err error) {  	u, err := c.GetAccountCurrentUser(ctx)  	if err != nil {  		return  	} -  	postContext := model.PostContext{  		DefaultVisibility: c.Session.Settings.DefaultVisibility,  		DefaultFormat:     c.Session.Settings.DefaultFormat, -		Formats:           svc.postFormats, +		Formats:           s.postFormats,  	} - -	commonData := svc.getCommonData(c, "Nav") +	commonData := s.getCommonData(c, "nav")  	commonData.Target = "main"  	data := &renderer.NavData{  		User:        u,  		CommonData:  commonData,  		PostContext: postContext,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.NavPage, data) +	return s.renderer.Render(rCtx, c, renderer.NavPage, data)  } -func (svc *service) ServeTimelinePage(c *model.Client, tType string, +func (s *service) TimelinePage(c *client, tType string, instance string,  	maxID string, minID string) (err error) {  	var nextLink, prevLink, title string @@ -236,10 +179,15 @@ func (svc *service) ServeTimelinePage(c *model.Client, tType string,  		statuses, err = c.GetTimelineDirect(ctx, &pg)  		title = "Direct Timeline"  	case "local": -		statuses, err = c.GetTimelinePublic(ctx, true, &pg) +		statuses, err = c.GetTimelinePublic(ctx, true, "", &pg)  		title = "Local Timeline" +	case "remote": +		if len(instance) > 0 { +			statuses, err = c.GetTimelinePublic(ctx, false, instance, &pg) +		} +		title = "Remote Timeline"  	case "twkn": -		statuses, err = c.GetTimelinePublic(ctx, false, &pg) +		statuses, err = c.GetTimelinePublic(ctx, false, "", &pg)  		title = "The Whole Known Network"  	}  	if err != nil { @@ -252,37 +200,29 @@ func (svc *service) ServeTimelinePage(c *model.Client, tType string,  		}  	} -	if len(maxID) > 0 && len(statuses) > 0 { -		prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", tType, -			statuses[0].ID) -	} - -	if len(minID) > 0 && len(pg.MinID) > 0 { -		newPg := &mastodon.Pagination{MinID: pg.MinID, Limit: 20} -		newStatuses, err := c.GetTimelineHome(ctx, newPg) -		if err != nil { -			return err -		} -		newLen := len(newStatuses) -		if newLen == 20 { -			prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", -				tType, pg.MinID) -		} else { -			i := 20 - newLen - 1 -			if len(statuses) > i { -				prevLink = fmt.Sprintf("/timeline/%s?min_id=%s", -					tType, 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)  		} +		prevLink = "/timeline/" + tType + "?" + v.Encode()  	} -	if len(pg.MaxID) > 0 && len(statuses) == 20 { -		nextLink = fmt.Sprintf("/timeline/%s?max_id=%s", tType, pg.MaxID) +	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) +		} +		nextLink = "/timeline/" + tType + "?" + v.Encode()  	} -	commonData := svc.getCommonData(c, tType+" timeline ") +	commonData := s.getCommonData(c, tType+" timeline ")  	data := &renderer.TimelineData{  		Title:      title, +		Type:       tType, +		Instance:   instance,  		Statuses:   statuses,  		NextLink:   nextLink,  		PrevLink:   prevLink, @@ -290,10 +230,10 @@ func (svc *service) ServeTimelinePage(c *model.Client, tType string,  	}  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.TimelinePage, data) +	return s.renderer.Render(rCtx, c, renderer.TimelinePage, data)  } -func (svc *service) ServeThreadPage(c *model.Client, id string, reply bool) (err error) { +func (s *service) ThreadPage(c *client, id string, reply bool) (err error) {  	var postContext model.PostContext  	status, err := c.GetStatus(ctx, id) @@ -324,14 +264,13 @@ func (svc *service) ServeThreadPage(c *model.Client, id string, reply bool) (err  		postContext = model.PostContext{  			DefaultVisibility: visibility,  			DefaultFormat:     c.Session.Settings.DefaultFormat, -			Formats:           svc.postFormats, +			Formats:           s.postFormats,  			ReplyContext: &model.ReplyContext{  				InReplyToID:     id,  				InReplyToName:   status.Account.Acct,  				ReplyContent:    content,  				ForceVisibility: isDirect,  			}, -			DarkMode: c.Session.Settings.DarkMode,  		}  	} @@ -354,7 +293,7 @@ func (svc *service) ServeThreadPage(c *model.Client, id string, reply bool) (err  		addToReplyMap(replies, statuses[i].InReplyToID, statuses[i].ID, i+1)  	} -	commonData := svc.getCommonData(c, "post by "+status.Account.DisplayName) +	commonData := s.getCommonData(c, "post by "+status.Account.DisplayName)  	data := &renderer.ThreadData{  		Statuses:    statuses,  		PostContext: postContext, @@ -363,51 +302,47 @@ func (svc *service) ServeThreadPage(c *model.Client, id string, reply bool) (err  	}  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.ThreadPage, data) +	return s.renderer.Render(rCtx, c, renderer.ThreadPage, data)  } -func (svc *service) ServeStatusPopup(c *model.Client, id string) (err error) { +func (svc *service) StatusPopup(c *client, id string) (err error) {  	status, err := c.GetStatus(ctx, id)  	if err != nil {  		return  	}  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.StatusPopup, status) +	return svc.renderer.Render(rCtx, c, renderer.StatusPopup, status)  } -func (svc *service) ServeLikedByPage(c *model.Client, id string) (err error) { +func (s *service) LikedByPage(c *client, id string) (err error) {  	likers, err := c.GetFavouritedBy(ctx, id, nil)  	if err != nil {  		return  	} - -	commonData := svc.getCommonData(c, "likes") +	commonData := s.getCommonData(c, "likes")  	data := &renderer.LikedByData{  		CommonData: commonData,  		Users:      likers,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.LikedByPage, data) +	return s.renderer.Render(rCtx, c, renderer.LikedByPage, data)  } -func (svc *service) ServeRetweetedByPage(c *model.Client, id string) (err error) { +func (s *service) RetweetedByPage(c *client, id string) (err error) {  	retweeters, err := c.GetRebloggedBy(ctx, id, nil)  	if err != nil {  		return  	} - -	commonData := svc.getCommonData(c, "retweets") +	commonData := s.getCommonData(c, "retweets")  	data := &renderer.RetweetedByData{  		CommonData: commonData,  		Users:      retweeters,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.RetweetedByPage, data) +	return s.renderer.Render(rCtx, c, renderer.RetweetedByPage, data)  } -func (svc *service) ServeNotificationPage(c *model.Client, maxID string, +func (s *service) NotificationPage(c *client, maxID string,  	minID string) (err error) {  	var nextLink string @@ -438,13 +373,12 @@ func (svc *service) ServeNotificationPage(c *model.Client, maxID string,  	if unreadCount > 0 {  		readID = notifications[0].ID  	} -  	if len(notifications) == 20 && len(pg.MaxID) > 0 {  		nextLink = "/notifications?max_id=" + pg.MaxID  	} -	commonData := svc.getCommonData(c, "notifications") -	commonData.AutoRefresh = c.Session.Settings.AutoRefreshNotifications +	commonData := s.getCommonData(c, "notifications") +	commonData.RefreshInterval = c.Session.Settings.NotificationInterval  	commonData.Target = "main"  	commonData.Count = unreadCount  	data := &renderer.NotificationData{ @@ -455,10 +389,10 @@ func (svc *service) ServeNotificationPage(c *model.Client, maxID string,  		CommonData:    commonData,  	}  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.NotificationPage, data) +	return s.renderer.Render(rCtx, c, renderer.NotificationPage, data)  } -func (svc *service) ServeUserPage(c *model.Client, id string, pageType string, +func (s *service) UserPage(c *client, id string, pageType string,  	maxID string, minID string) (err error) {  	var nextLink string @@ -561,6 +495,18 @@ func (svc *service) ServeUserPage(c *model.Client, id string, pageType string,  			nextLink = fmt.Sprintf("/user/%s/likes?max_id=%s",  				id, pg.MaxID)  		} +	case "requests": +		if !isCurrent { +			return errInvalidArgument +		} +		users, err = c.GetFollowRequests(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  	} @@ -571,7 +517,7 @@ func (svc *service) ServeUserPage(c *model.Client, id string, pageType string,  		}  	} -	commonData := svc.getCommonData(c, user.DisplayName) +	commonData := s.getCommonData(c, user.DisplayName)  	data := &renderer.UserData{  		User:       user,  		IsCurrent:  isCurrent, @@ -582,10 +528,10 @@ func (svc *service) ServeUserPage(c *model.Client, id string, pageType string,  		CommonData: commonData,  	}  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.UserPage, data) +	return s.renderer.Render(rCtx, c, renderer.UserPage, data)  } -func (svc *service) ServeUserSearchPage(c *model.Client, +func (s *service) UserSearchPage(c *client,  	id string, q string, offset int) (err error) {  	var nextLink string @@ -608,7 +554,8 @@ func (svc *service) ServeUserSearchPage(c *model.Client,  	if len(results.Statuses) == 20 {  		offset += 20 -		nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id, url.QueryEscape(q), offset) +		nextLink = fmt.Sprintf("/usersearch/%s?q=%s&offset=%d", id, +			url.QueryEscape(q), offset)  	}  	qq := template.HTMLEscapeString(q) @@ -616,7 +563,7 @@ func (svc *service) ServeUserSearchPage(c *model.Client,  		title += " \"" + qq + "\""  	} -	commonData := svc.getCommonData(c, title) +	commonData := s.getCommonData(c, title)  	data := &renderer.UserSearchData{  		CommonData: commonData,  		User:       user, @@ -624,38 +571,34 @@ func (svc *service) ServeUserSearchPage(c *model.Client,  		Statuses:   results.Statuses,  		NextLink:   nextLink,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.UserSearchPage, data) +	return s.renderer.Render(rCtx, c, renderer.UserSearchPage, data)  } -func (svc *service) ServeAboutPage(c *model.Client) (err error) { -	commonData := svc.getCommonData(c, "about") +func (s *service) AboutPage(c *client) (err error) { +	commonData := s.getCommonData(c, "about")  	data := &renderer.AboutData{  		CommonData: commonData,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.AboutPage, data) +	return s.renderer.Render(rCtx, c, renderer.AboutPage, data)  } -func (svc *service) ServeEmojiPage(c *model.Client) (err error) { +func (s *service) EmojiPage(c *client) (err error) {  	emojis, err := c.GetInstanceEmojis(ctx)  	if err != nil {  		return  	} - -	commonData := svc.getCommonData(c, "emojis") +	commonData := s.getCommonData(c, "emojis")  	data := &renderer.EmojiData{  		Emojis:     emojis,  		CommonData: commonData,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.EmojiPage, data) +	return s.renderer.Render(rCtx, c, renderer.EmojiPage, data)  } -func (svc *service) ServeSearchPage(c *model.Client, +func (s *service) SearchPage(c *client,  	q string, qType string, offset int) (err error) {  	var nextLink string @@ -674,7 +617,8 @@ func (svc *service) ServeSearchPage(c *model.Client,  	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", url.QueryEscape(q), qType, offset) +		nextLink = fmt.Sprintf("/search?q=%s&type=%s&offset=%d", +			url.QueryEscape(q), qType, offset)  	}  	qq := template.HTMLEscapeString(q) @@ -682,7 +626,7 @@ func (svc *service) ServeSearchPage(c *model.Client,  		title += " \"" + qq + "\""  	} -	commonData := svc.getCommonData(c, title) +	commonData := s.getCommonData(c, title)  	data := &renderer.SearchData{  		CommonData: commonData,  		Q:          qq, @@ -691,34 +635,30 @@ func (svc *service) ServeSearchPage(c *model.Client,  		Statuses:   results.Statuses,  		NextLink:   nextLink,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.SearchPage, data) +	return s.renderer.Render(rCtx, c, renderer.SearchPage, data)  } -func (svc *service) ServeSettingsPage(c *model.Client) (err error) { -	commonData := svc.getCommonData(c, "settings") +func (s *service) SettingsPage(c *client) (err error) { +	commonData := s.getCommonData(c, "settings")  	data := &renderer.SettingsData{  		CommonData:  commonData,  		Settings:    &c.Session.Settings, -		PostFormats: svc.postFormats, +		PostFormats: s.postFormats,  	} -  	rCtx := getRendererContext(c) -	return svc.renderer.Render(rCtx, c.Writer, renderer.SettingsPage, data) +	return s.renderer.Render(rCtx, c, renderer.SettingsPage, data)  } -func (svc *service) SingleInstance() (instance string, ok bool) { -	if len(svc.singleInstance) > 0 { -		instance = svc.singleInstance +func (s *service) SingleInstance() (instance string, ok bool) { +	if len(s.singleInstance) > 0 { +		instance = s.singleInstance  		ok = true  	}  	return  } -func (svc *service) NewSession(instance string) ( -	redirectUrl string, sessionID string, err error) { - +func (s *service) NewSession(instance string) (rurl string, sid string, err error) {  	var instanceURL string  	if strings.HasPrefix(instance, "https://") {  		instanceURL = instance @@ -727,53 +667,48 @@ func (svc *service) NewSession(instance string) (  		instanceURL = "https://" + instance  	} -	sessionID, err = util.NewSessionID() +	sid, err = util.NewSessionID()  	if err != nil {  		return  	} -  	csrfToken, err := util.NewCSRFToken()  	if err != nil {  		return  	}  	session := model.Session{ -		ID:             sessionID, +		ID:             sid,  		InstanceDomain: instance,  		CSRFToken:      csrfToken,  		Settings:       *model.NewSettings(),  	} - -	err = svc.sessionRepo.Add(session) +	err = s.sessionRepo.Add(session)  	if err != nil {  		return  	} -	app, err := svc.appRepo.Get(instance) +	app, err := s.appRepo.Get(instance)  	if err != nil {  		if err != model.ErrAppNotFound {  			return  		} -  		mastoApp, err := mastodon.RegisterApp(ctx, &mastodon.AppConfig{  			Server:       instanceURL, -			ClientName:   svc.clientName, -			Scopes:       svc.clientScope, -			Website:      svc.clientWebsite, -			RedirectURIs: svc.clientWebsite + "/oauth_callback", +			ClientName:   s.clientName, +			Scopes:       s.clientScope, +			Website:      s.clientWebsite, +			RedirectURIs: s.clientWebsite + "/oauth_callback",  		})  		if err != nil {  			return "", "", err  		} -  		app = model.App{  			InstanceDomain: instance,  			InstanceURL:    instanceURL,  			ClientID:       mastoApp.ClientID,  			ClientSecret:   mastoApp.ClientSecret,  		} - -		err = svc.appRepo.Add(app) +		err = s.appRepo.Add(app)  		if err != nil {  			return "", "", err  		} @@ -788,23 +723,21 @@ func (svc *service) NewSession(instance string) (  	q.Set("scope", "read write follow")  	q.Set("client_id", app.ClientID)  	q.Set("response_type", "code") -	q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback") +	q.Set("redirect_uri", s.clientWebsite+"/oauth_callback")  	u.RawQuery = q.Encode() -	redirectUrl = instanceURL + u.String() - +	rurl = instanceURL + u.String()  	return  } -func (svc *service) Signin(c *model.Client, sessionID string, -	code string) (token string, userID string, err error) { +func (s *service) Signin(c *client, code string) (token string, +	userID string, err error) {  	if len(code) < 1 {  		err = errInvalidArgument  		return  	} - -	err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback") +	err = c.AuthenticateToken(ctx, code, s.clientWebsite+"/oauth_callback")  	if err != nil {  		return  	} @@ -815,17 +748,16 @@ func (svc *service) Signin(c *model.Client, sessionID string,  		return  	}  	userID = u.ID -  	return  } -func (svc *service) Signout(c *model.Client) (err error) { -	svc.sessionRepo.Remove(c.Session.ID) +func (s *service) Signout(c *client) (err error) { +	s.sessionRepo.Remove(c.Session.ID)  	return  } -func (svc *service) Post(c *model.Client, content string, -	replyToID string, format string, visibility string, isNSFW bool, +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 @@ -845,136 +777,143 @@ func (svc *service) Post(c *model.Client, content string,  		Visibility:  visibility,  		Sensitive:   isNSFW,  	} - -	s, err := c.PostStatus(ctx, tweet) +	st, err := c.PostStatus(ctx, tweet)  	if err != nil {  		return  	} - -	return s.ID, nil +	return st.ID, nil  } -func (svc *service) Like(c *model.Client, id string) (count int64, err error) { -	s, err := c.Favourite(ctx, id) +func (s *service) Like(c *client, id string) (count int64, err error) { +	st, err := c.Favourite(ctx, id)  	if err != nil {  		return  	} -	count = s.FavouritesCount +	count = st.FavouritesCount  	return  } -func (svc *service) UnLike(c *model.Client, id string) (count int64, err error) { -	s, err := c.Unfavourite(ctx, id) +func (s *service) UnLike(c *client, id string) (count int64, err error) { +	st, err := c.Unfavourite(ctx, id)  	if err != nil {  		return  	} -	count = s.FavouritesCount +	count = st.FavouritesCount  	return  } -func (svc *service) Retweet(c *model.Client, id string) (count int64, err error) { -	s, err := c.Reblog(ctx, id) +func (s *service) Retweet(c *client, id string) (count int64, err error) { +	st, err := c.Reblog(ctx, id)  	if err != nil {  		return  	} -	if s.Reblog != nil { -		count = s.Reblog.ReblogsCount +	if st.Reblog != nil { +		count = st.Reblog.ReblogsCount  	}  	return  } -func (svc *service) UnRetweet(c *model.Client, id string) ( +func (s *service) UnRetweet(c *client, id string) (  	count int64, err error) { -	s, err := c.Unreblog(ctx, id) +	st, err := c.Unreblog(ctx, id)  	if err != nil {  		return  	} -	count = s.ReblogsCount +	count = st.ReblogsCount  	return  } -func (svc *service) Vote(c *model.Client, id string, choices []string) (err error) { +func (s *service) Vote(c *client, id string, choices []string) (err error) {  	_, err = c.Vote(ctx, id, choices) -	if err != nil { -		return -	}  	return  } -func (svc *service) Follow(c *model.Client, id string, reblogs *bool) (err error) { +func (s *service) Follow(c *client, id string, reblogs *bool) (err error) {  	_, err = c.AccountFollow(ctx, id, reblogs)  	return  } -func (svc *service) UnFollow(c *model.Client, id string) (err error) { +func (s *service) UnFollow(c *client, id string) (err error) {  	_, err = c.AccountUnfollow(ctx, id)  	return  } -func (svc *service) Mute(c *model.Client, id string) (err error) { +func (s *service) Accept(c *client, id string) (err error) { +	return c.FollowRequestAuthorize(ctx, id) +} + +func (s *service) Reject(c *client, id string) (err error) { +	return c.FollowRequestReject(ctx, id) +} + +func (s *service) Mute(c *client, id string) (err error) {  	_, err = c.AccountMute(ctx, id)  	return  } -func (svc *service) UnMute(c *model.Client, id string) (err error) { +func (s *service) UnMute(c *client, id string) (err error) {  	_, err = c.AccountUnmute(ctx, id)  	return  } -func (svc *service) Block(c *model.Client, id string) (err error) { +func (s *service) Block(c *client, id string) (err error) {  	_, err = c.AccountBlock(ctx, id)  	return  } -func (svc *service) UnBlock(c *model.Client, id string) (err error) { +func (s *service) UnBlock(c *client, id string) (err error) {  	_, err = c.AccountUnblock(ctx, id)  	return  } -func (svc *service) Subscribe(c *model.Client, id string) (err error) { +func (s *service) Subscribe(c *client, id string) (err error) {  	_, err = c.Subscribe(ctx, id)  	return  } -func (svc *service) UnSubscribe(c *model.Client, id string) (err error) { +func (s *service) UnSubscribe(c *client, id string) (err error) {  	_, err = c.UnSubscribe(ctx, id)  	return  } -func (svc *service) SaveSettings(c *model.Client, s *model.Settings) (err error) { -	session, err := svc.sessionRepo.Get(c.Session.ID) +func (s *service) SaveSettings(c *client, settings *model.Settings) (err error) { +	switch settings.NotificationInterval { +	case 0, 30, 60, 120, 300, 600: +	default: +		return errInvalidArgument +	} +	session, err := s.sessionRepo.Get(c.Session.ID)  	if err != nil {  		return  	} - -	session.Settings = *s -	return svc.sessionRepo.Add(session) +	session.Settings = *settings +	return s.sessionRepo.Add(session)  } -func (svc *service) MuteConversation(c *model.Client, id string) (err error) { +func (s *service) MuteConversation(c *client, id string) (err error) {  	_, err = c.MuteConversation(ctx, id)  	return  } -func (svc *service) UnMuteConversation(c *model.Client, id string) (err error) { +func (s *service) UnMuteConversation(c *client, id string) (err error) {  	_, err = c.UnmuteConversation(ctx, id)  	return  } -func (svc *service) Delete(c *model.Client, id string) (err error) { +func (s *service) Delete(c *client, id string) (err error) {  	return c.DeleteStatus(ctx, id)  } -func (svc *service) ReadNotifications(c *model.Client, maxID string) (err error) { +func (s *service) ReadNotifications(c *client, maxID string) (err error) {  	return c.ReadNotifications(ctx, maxID)  } -func (svc *service) Bookmark(c *model.Client, id string) (err error) { +func (s *service) Bookmark(c *client, id string) (err error) {  	_, err = c.Bookmark(ctx, id)  	return  } -func (svc *service) UnBookmark(c *model.Client, id string) (err error) { +func (s *service) UnBookmark(c *client, id string) (err error) {  	_, err = c.Unbookmark(ctx, id)  	return  } diff --git a/service/transport.go b/service/transport.go index bacb19d..9841650 100644 --- a/service/transport.go +++ b/service/transport.go @@ -2,61 +2,66 @@ package service  import (  	"encoding/json" -	"io" -	"mime/multipart" +	"errors" +	"log"  	"net/http"  	"strconv"  	"time" +	"bloat/mastodon"  	"bloat/model"  	"github.com/gorilla/mux"  ) +var ( +	errInvalidSession   = errors.New("invalid session") +	errInvalidCSRFToken = errors.New("invalid csrf token") +) +  const (  	sessionExp = 365 * 24 * time.Hour  ) -func newClient(w io.Writer, req *http.Request, csrfToken string) *model.Client { -	var sessionID string -	if req != nil { -		c, err := req.Cookie("session_id") -		if err == nil { -			sessionID = c.Value -		} -	} -	return &model.Client{ -		Writer: w, -		Ctx: model.ClientCtx{ -			SessionID: sessionID, -			CSRFToken: csrfToken, -		}, -	} +type respType int + +const ( +	HTML respType = iota +	JSON +) + +type authType int + +const ( +	NOAUTH authType = iota +	SESSION +	CSRF +) + +type client struct { +	*mastodon.Client +	http.ResponseWriter +	Req       *http.Request +	CSRFToken string +	Session   model.Session  } -func setSessionCookie(w http.ResponseWriter, sessionID string, exp time.Duration) { +func (c *client) url() string { +	return c.Req.URL.RequestURI() +} + +func setSessionCookie(w http.ResponseWriter, sid string, exp time.Duration) {  	http.SetCookie(w, &http.Cookie{  		Name:    "session_id", -		Value:   sessionID, +		Value:   sid,  		Expires: time.Now().Add(exp),  	})  } -func getMultipartFormValue(mf *multipart.Form, key string) (val string) { -	vals, ok := mf.Value[key] -	if !ok { -		return "" -	} -	if len(vals) < 1 { -		return "" -	} -	return vals[0] -} - -func serveJson(w io.Writer, data interface{}) (err error) { -	var d = make(map[string]interface{}) -	d["data"] = data -	return json.NewEncoder(w).Encode(d) +func writeJson(c *client, data interface{}) error { +	return json.NewEncoder(c).Encode(map[string]interface{}{ +		"data": data, +	})  }  func serveJsonError(w http.ResponseWriter, err error) { @@ -68,745 +73,578 @@ func serveJsonError(w http.ResponseWriter, err error) {  	return  } -func NewHandler(s Service, staticDir string) http.Handler { -	r := mux.NewRouter() +func redirect(c *client, url string) { +	c.Header().Add("Location", url) +	c.WriteHeader(http.StatusFound) +} -	rootPage := func(w http.ResponseWriter, req *http.Request) { -		sessionID, _ := req.Cookie("session_id") -		if sessionID != nil && len(sessionID.Value) > 0 { -			c := newClient(w, req, "") -			err := s.ServeRootPage(c) -			if err != nil { -				if err == errInvalidAccessToken { -					w.Header().Add("Location", "/signin") -					w.WriteHeader(http.StatusFound) -					return -				} -				w.WriteHeader(http.StatusInternalServerError) -				s.ServeErrorPage(c, err) -				return -			} -		} else { -			w.Header().Add("Location", "/signin") -			w.WriteHeader(http.StatusFound) -		} -	} +func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler { +	r := mux.NewRouter() -	navPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		err := s.ServeNavPage(c) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +	writeError := func(c *client, err error, t respType) { +		switch t { +		case HTML: +			c.WriteHeader(http.StatusInternalServerError) +			s.ErrorPage(c, err) +		case JSON: +			c.WriteHeader(http.StatusInternalServerError) +			json.NewEncoder(c).Encode(map[string]string{ +				"error": err.Error(), +			})  		}  	} -	signinPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, nil, "") -		instance, ok := s.SingleInstance() -		if ok { -			url, sessionID, err := s.NewSession(instance) +	authenticate := func(c *client, t authType) error { +		if t >= SESSION { +			cookie, err := c.Req.Cookie("session_id") +			if err != nil || len(cookie.Value) < 1 { +				return errInvalidSession +			} +			c.Session, err = s.sessionRepo.Get(cookie.Value)  			if err != nil { -				w.WriteHeader(http.StatusInternalServerError) -				s.ServeErrorPage(c, err) -				return +				return errInvalidSession  			} - -			setSessionCookie(w, sessionID, sessionExp) -			w.Header().Add("Location", url) -			w.WriteHeader(http.StatusFound) -		} else { -			err := s.ServeSigninPage(c) +			app, err := s.appRepo.Get(c.Session.InstanceDomain)  			if err != nil { -				w.WriteHeader(http.StatusInternalServerError) -				s.ServeErrorPage(c, err) -				return +				return err +			} +			c.Client = mastodon.NewClient(&mastodon.Config{ +				Server:       app.InstanceURL, +				ClientID:     app.ClientID, +				ClientSecret: app.ClientSecret, +				AccessToken:  c.Session.AccessToken, +			}) +		} +		if t >= CSRF { +			c.CSRFToken = c.Req.FormValue("csrf_token") +			if len(c.CSRFToken) < 1 || c.CSRFToken != c.Session.CSRFToken { +				return errInvalidCSRFToken  			}  		} +		return nil  	} -	timelinePage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		tType, _ := mux.Vars(req)["type"] -		maxID := req.URL.Query().Get("max_id") -		minID := req.URL.Query().Get("min_id") - -		err := s.ServeTimelinePage(c, tType, maxID, minID) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} - -	defaultTimelinePage := func(w http.ResponseWriter, req *http.Request) { -		w.Header().Add("Location", "/timeline/home") -		w.WriteHeader(http.StatusFound) -	} - -	threadPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		id, _ := mux.Vars(req)["id"] -		reply := req.URL.Query().Get("reply") - -		err := s.ServeThreadPage(c, id, len(reply) > 1) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} - -	likedByPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		id, _ := mux.Vars(req)["id"] - -		err := s.ServeLikedByPage(c, id) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} - -	retweetedByPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		id, _ := mux.Vars(req)["id"] - -		err := s.ServeRetweetedByPage(c, id) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} - -	notificationsPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		maxID := req.URL.Query().Get("max_id") -		minID := req.URL.Query().Get("min_id") - -		err := s.ServeNotificationPage(c, maxID, minID) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} - -	userPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		id, _ := mux.Vars(req)["id"] -		pageType, _ := mux.Vars(req)["type"] -		maxID := req.URL.Query().Get("max_id") -		minID := req.URL.Query().Get("min_id") +	handle := func(f func(c *client) error, at authType, rt respType) http.HandlerFunc { +		return func(w http.ResponseWriter, req *http.Request) { +			var err error +			c := &client{Req: req, ResponseWriter: w} -		err := s.ServeUserPage(c, id, pageType, maxID, minID) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} +			defer func(begin time.Time) { +				logger.Printf("path=%s, err=%v, took=%v\n", +					req.URL.Path, err, time.Since(begin)) +			}(time.Now()) -	userSearchPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		id, _ := mux.Vars(req)["id"] -		q := req.URL.Query().Get("q") -		offsetStr := req.URL.Query().Get("offset") +			var ct string +			switch rt { +			case HTML: +				ct = "text/html; charset=utf-8" +			case JSON: +				ct = "application/json" +			} +			c.Header().Add("Content-Type", ct) -		var offset int -		var err error -		if len(offsetStr) > 1 { -			offset, err = strconv.Atoi(offsetStr) +			err = authenticate(c, at)  			if err != nil { -				w.WriteHeader(http.StatusInternalServerError) -				s.ServeErrorPage(c, err) +				writeError(c, err, rt)  				return  			} -		} - -		err = s.ServeUserSearchPage(c, id, q, offset) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} - -	aboutPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		err := s.ServeAboutPage(c) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} - -	emojisPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") - -		err := s.ServeEmojiPage(c) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} -	} - -	searchPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		q := req.URL.Query().Get("q") -		qType := req.URL.Query().Get("type") -		offsetStr := req.URL.Query().Get("offset") - -		var offset int -		var err error -		if len(offsetStr) > 1 { -			offset, err = strconv.Atoi(offsetStr) +			err = f(c)  			if err != nil { -				w.WriteHeader(http.StatusInternalServerError) -				s.ServeErrorPage(c, err) +				writeError(c, err, rt)  				return  			}  		} - -		err = s.ServeSearchPage(c, q, qType, offset) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		}  	} -	settingsPage := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") - -		err := s.ServeSettingsPage(c) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +	rootPage := handle(func(c *client) error { +		sid, _ := c.Req.Cookie("session_id") +		if sid == nil || len(sid.Value) < 0 { +			redirect(c, "/signin") +			return nil  		} -	} - -	signin := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, nil, "") -		instance := req.FormValue("instance") - -		url, sessionID, err := s.NewSession(instance) +		session, err := s.sessionRepo.Get(sid.Value)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			if err == errInvalidSession { +				redirect(c, "/signin") +				return nil +			} +			return err  		} - -		setSessionCookie(w, sessionID, sessionExp) -		w.Header().Add("Location", url) -		w.WriteHeader(http.StatusFound) -	} - -	oauthCallback := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		token := req.URL.Query().Get("code") - -		_, _, err := s.Signin(c, "", token) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +		if len(session.AccessToken) < 1 { +			redirect(c, "/signin") +			return nil  		} +		return s.RootPage(c) +	}, NOAUTH, HTML) -		w.Header().Add("Location", "/") -		w.WriteHeader(http.StatusFound) -	} - -	post := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		err := req.ParseMultipartForm(4 << 20) -		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return -		} +	navPage := handle(func(c *client) error { +		return s.NavPage(c) +	}, SESSION, HTML) -		c = newClient(w, req, -			getMultipartFormValue(req.MultipartForm, "csrf_token")) -		content := getMultipartFormValue(req.MultipartForm, "content") -		replyToID := getMultipartFormValue(req.MultipartForm, "reply_to_id") -		format := getMultipartFormValue(req.MultipartForm, "format") -		visibility := getMultipartFormValue(req.MultipartForm, "visibility") -		isNSFW := "on" == getMultipartFormValue(req.MultipartForm, "is_nsfw") -		files := req.MultipartForm.File["attachments"] +	signinPage := handle(func(c *client) error { +		instance, ok := s.SingleInstance() +		if !ok { +			return s.SigninPage(c) +		} +		url, sid, err := s.NewSession(instance) +		if err != nil { +			return err +		} +		setSessionCookie(c, sid, sessionExp) +		redirect(c, url) +		return nil +	}, NOAUTH, HTML) + +	timelinePage := handle(func(c *client) error { +		tType, _ := mux.Vars(c.Req)["type"] +		q := c.Req.URL.Query() +		instance := q.Get("instance") +		maxID := q.Get("max_id") +		minID := q.Get("min_id") +		return s.TimelinePage(c, tType, instance, maxID, minID) +	}, SESSION, HTML) + +	defaultTimelinePage := handle(func(c *client) error { +		redirect(c, "/timeline/home") +		return nil +	}, SESSION, HTML) + +	threadPage := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		q := c.Req.URL.Query() +		reply := q.Get("reply") +		return s.ThreadPage(c, id, len(reply) > 1) +	}, SESSION, HTML) + +	likedByPage := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		return s.LikedByPage(c, id) +	}, SESSION, HTML) + +	retweetedByPage := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		return s.RetweetedByPage(c, id) +	}, SESSION, HTML) + +	notificationsPage := handle(func(c *client) error { +		q := c.Req.URL.Query() +		maxID := q.Get("max_id") +		minID := q.Get("min_id") +		return s.NotificationPage(c, maxID, minID) +	}, SESSION, HTML) + +	userPage := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		pageType, _ := mux.Vars(c.Req)["type"] +		q := c.Req.URL.Query() +		maxID := q.Get("max_id") +		minID := q.Get("min_id") +		return s.UserPage(c, id, pageType, maxID, minID) +	}, SESSION, HTML) + +	userSearchPage := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		q := c.Req.URL.Query() +		sq := q.Get("q") +		offset, _ := strconv.Atoi(q.Get("offset")) +		return s.UserSearchPage(c, id, sq, offset) +	}, SESSION, HTML) + +	aboutPage := handle(func(c *client) error { +		return s.AboutPage(c) +	}, SESSION, HTML) + +	emojisPage := handle(func(c *client) error { +		return s.EmojiPage(c) +	}, SESSION, HTML) + +	searchPage := handle(func(c *client) error { +		q := c.Req.URL.Query() +		sq := q.Get("q") +		qType := q.Get("type") +		offset, _ := strconv.Atoi(q.Get("offset")) +		return s.SearchPage(c, sq, qType, offset) +	}, SESSION, HTML) + +	settingsPage := handle(func(c *client) error { +		return s.SettingsPage(c) +	}, SESSION, HTML) + +	signin := handle(func(c *client) error { +		instance := c.Req.FormValue("instance") +		url, sid, err := s.NewSession(instance) +		if err != nil { +			return err +		} +		setSessionCookie(c, sid, sessionExp) +		redirect(c, url) +		return nil +	}, NOAUTH, HTML) + +	oauthCallback := handle(func(c *client) error { +		q := c.Req.URL.Query() +		token := q.Get("code") +		token, userID, err := s.Signin(c, token) +		if err != nil { +			return err +		} + +		c.Session.AccessToken = token +		c.Session.UserID = userID +		err = s.sessionRepo.Add(c.Session) +		if err != nil { +			return err +		} + +		redirect(c, "/") +		return nil +	}, SESSION, HTML) + +	post := handle(func(c *client) error { +		content := c.Req.FormValue("content") +		replyToID := c.Req.FormValue("reply_to_id") +		format := c.Req.FormValue("format") +		visibility := c.Req.FormValue("visibility") +		isNSFW := c.Req.FormValue("is_nsfw") == "on" +		files := c.Req.MultipartForm.File["attachments"]  		id, err := s.Post(c, content, replyToID, format, visibility, isNSFW, files)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} -		location := req.Header.Get("Referer") +		location := c.Req.FormValue("referrer")  		if len(replyToID) > 0 {  			location = "/thread/" + replyToID + "#status-" + id  		} -		w.Header().Add("Location", location) -		w.WriteHeader(http.StatusFound) -	} - -	like := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] -		retweetedByID := req.FormValue("retweeted_by_id") +		redirect(c, location) +		return nil +	}, CSRF, HTML) +	like := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		rid := c.Req.FormValue("retweeted_by_id")  		_, err := s.Like(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} - -		rID := id -		if len(retweetedByID) > 0 { -			rID = retweetedByID +		if len(rid) > 0 { +			id = rid  		} -		w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID) -		w.WriteHeader(http.StatusFound) -	} - -	unlike := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] -		retweetedByID := req.FormValue("retweeted_by_id") +		redirect(c, c.Req.FormValue("referrer")+"#status-"+id) +		return nil +	}, CSRF, HTML) +	unlike := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		rid := c.Req.FormValue("retweeted_by_id")  		_, err := s.UnLike(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} - -		rID := id -		if len(retweetedByID) > 0 { -			rID = retweetedByID +		if len(rid) > 0 { +			id = rid  		} -		w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID) -		w.WriteHeader(http.StatusFound) -	} - -	retweet := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] -		retweetedByID := req.FormValue("retweeted_by_id") +		redirect(c, c.Req.FormValue("referrer")+"#status-"+id) +		return nil +	}, CSRF, HTML) +	retweet := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		rid := c.Req.FormValue("retweeted_by_id")  		_, err := s.Retweet(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} - -		rID := id -		if len(retweetedByID) > 0 { -			rID = retweetedByID +		if len(rid) > 0 { +			id = rid  		} -		w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID) -		w.WriteHeader(http.StatusFound) -	} - -	unretweet := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] -		retweetedByID := req.FormValue("retweeted_by_id") +		redirect(c, c.Req.FormValue("referrer")+"#status-"+id) +		return nil +	}, CSRF, HTML) +	unretweet := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		rid := c.Req.FormValue("retweeted_by_id")  		_, err := s.UnRetweet(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} - -		rID := id -		if len(retweetedByID) > 0 { -			rID = retweetedByID +		if len(rid) > 0 { +			id = rid  		} +		redirect(c, c.Req.FormValue("referrer")+"#status-"+id) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID) -		w.WriteHeader(http.StatusFound) -	} - -	vote := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] -		statusID := req.FormValue("status_id") -		choices, _ := req.PostForm["choices"] - +	vote := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		statusID := c.Req.FormValue("status_id") +		choices, _ := c.Req.PostForm["choices"]  		err := s.Vote(c, id, choices)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")+"#status-"+statusID) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+statusID) -		w.WriteHeader(http.StatusFound) -	} - -	follow := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	follow := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		q := c.Req.URL.Query()  		var reblogs *bool -		r, ok := req.URL.Query()["reblogs"] -		if ok && len(r) > 0 { +		if r, ok := q["reblogs"]; ok && len(r) > 0 {  			reblogs = new(bool)  			*reblogs = r[0] == "true"  		} -  		err := s.Follow(c, id, reblogs)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	unfollow := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	unfollow := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.UnFollow(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} +	accept := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		err := s.Accept(c, id) +		if err != nil { +			return err +		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -	mute := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] +	reject := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		err := s.Reject(c, id) +		if err != nil { +			return err +		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) +	mute := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.Mute(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	unMute := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	unMute := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.UnMute(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	block := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	block := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.Block(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	unBlock := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	unBlock := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.UnBlock(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	subscribe := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	subscribe := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.Subscribe(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	unSubscribe := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	unSubscribe := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.UnSubscribe(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	settings := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		visibility := req.FormValue("visibility") -		format := req.FormValue("format") -		copyScope := req.FormValue("copy_scope") == "true" -		threadInNewTab := req.FormValue("thread_in_new_tab") == "true" -		hideAttachments := req.FormValue("hide_attachments") == "true" -		maskNSFW := req.FormValue("mask_nsfw") == "true" -		arn := req.FormValue("auto_refresh_notifications") == "true" -		fluorideMode := req.FormValue("fluoride_mode") == "true" -		darkMode := req.FormValue("dark_mode") == "true" -		antiDopamineMode := req.FormValue("anti_dopamine_mode") == "true" +	settings := handle(func(c *client) error { +		visibility := c.Req.FormValue("visibility") +		format := c.Req.FormValue("format") +		copyScope := c.Req.FormValue("copy_scope") == "true" +		threadInNewTab := c.Req.FormValue("thread_in_new_tab") == "true" +		hideAttachments := c.Req.FormValue("hide_attachments") == "true" +		maskNSFW := c.Req.FormValue("mask_nsfw") == "true" +		ni, _ := strconv.Atoi(c.Req.FormValue("notification_interval")) +		fluorideMode := c.Req.FormValue("fluoride_mode") == "true" +		darkMode := c.Req.FormValue("dark_mode") == "true" +		antiDopamineMode := c.Req.FormValue("anti_dopamine_mode") == "true"  		settings := &model.Settings{ -			DefaultVisibility:        visibility, -			DefaultFormat:            format, -			CopyScope:                copyScope, -			ThreadInNewTab:           threadInNewTab, -			HideAttachments:          hideAttachments, -			MaskNSFW:                 maskNSFW, -			AutoRefreshNotifications: arn, -			FluorideMode:             fluorideMode, -			DarkMode:                 darkMode, -			AntiDopamineMode:         antiDopamineMode, +			DefaultVisibility:    visibility, +			DefaultFormat:        format, +			CopyScope:            copyScope, +			ThreadInNewTab:       threadInNewTab, +			HideAttachments:      hideAttachments, +			MaskNSFW:             maskNSFW, +			NotificationInterval: ni, +			FluorideMode:         fluorideMode, +			DarkMode:             darkMode, +			AntiDopamineMode:     antiDopamineMode,  		}  		err := s.SaveSettings(c, settings)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, "/") +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", "/") -		w.WriteHeader(http.StatusFound) -	} - -	muteConversation := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	muteConversation := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.MuteConversation(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	unMuteConversation := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	unMuteConversation := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.UnMuteConversation(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	delete := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	delete := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		err := s.Delete(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	readNotifications := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		maxID := req.URL.Query().Get("max_id") - +	readNotifications := handle(func(c *client) error { +		q := c.Req.URL.Query() +		maxID := q.Get("max_id")  		err := s.ReadNotifications(c, maxID)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} +		redirect(c, c.Req.FormValue("referrer")) +		return nil +	}, CSRF, HTML) -		w.Header().Add("Location", req.Header.Get("Referer")) -		w.WriteHeader(http.StatusFound) -	} - -	bookmark := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] -		retweetedByID := req.FormValue("retweeted_by_id") - +	bookmark := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		rid := c.Req.FormValue("retweeted_by_id")  		err := s.Bookmark(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} - -		rID := id -		if len(retweetedByID) > 0 { -			rID = retweetedByID +		if len(rid) > 0 { +			id = rid  		} -		w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID) -		w.WriteHeader(http.StatusFound) -	} - -	unBookmark := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] -		retweetedByID := req.FormValue("retweeted_by_id") +		redirect(c, c.Req.FormValue("referrer")+"#status-"+id) +		return nil +	}, CSRF, HTML) +	unBookmark := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		rid := c.Req.FormValue("retweeted_by_id")  		err := s.UnBookmark(c, id)  		if err != nil { -			w.WriteHeader(http.StatusInternalServerError) -			s.ServeErrorPage(c, err) -			return +			return err  		} - -		rID := id -		if len(retweetedByID) > 0 { -			rID = retweetedByID +		if len(rid) > 0 { +			id = rid  		} -		w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID) -		w.WriteHeader(http.StatusFound) -	} - -	signout := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) +		redirect(c, c.Req.FormValue("referrer")+"#status-"+id) +		return nil +	}, CSRF, HTML) +	signout := handle(func(c *client) error {  		s.Signout(c) +		setSessionCookie(c, "", 0) +		redirect(c, "/") +		return nil +	}, CSRF, HTML) -		setSessionCookie(w, "", 0) -		w.Header().Add("Location", "/") -		w.WriteHeader(http.StatusFound) -	} - -	fLike := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	fLike := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		count, err := s.Like(c, id)  		if err != nil { -			serveJsonError(w, err) -			return -		} - -		err = serveJson(w, count) -		if err != nil { -			serveJsonError(w, err) -			return +			return err  		} -	} - -	fUnlike := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] +		return writeJson(c, count) +	}, CSRF, JSON) +	fUnlike := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		count, err := s.UnLike(c, id)  		if err != nil { -			serveJsonError(w, err) -			return +			return err  		} +		return writeJson(c, count) +	}, CSRF, JSON) -		err = serveJson(w, count) -		if err != nil { -			serveJsonError(w, err) -			return -		} -	} - -	fRetweet := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] - +	fRetweet := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		count, err := s.Retweet(c, id)  		if err != nil { -			serveJsonError(w, err) -			return -		} - -		err = serveJson(w, count) -		if err != nil { -			serveJsonError(w, err) -			return +			return err  		} -	} - -	fUnretweet := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, req.FormValue("csrf_token")) -		id, _ := mux.Vars(req)["id"] +		return writeJson(c, count) +	}, CSRF, JSON) +	fUnretweet := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"]  		count, err := s.UnRetweet(c, id)  		if err != nil { -			serveJsonError(w, err) -			return -		} - -		err = serveJson(w, count) -		if err != nil { -			serveJsonError(w, err) -			return +			return err  		} -	} - -	statusPopup := func(w http.ResponseWriter, req *http.Request) { -		c := newClient(w, req, "") -		id, _ := mux.Vars(req)["id"] +		return writeJson(c, count) +	}, CSRF, JSON) -		err := s.ServeStatusPopup(c, id) -		if err != nil { -			serveJsonError(w, err) -			return -		} -	} +	fStatus := handle(func(c *client) error { +		id, _ := mux.Vars(c.Req)["id"] +		return s.StatusPopup(c, id) +	}, SESSION, JSON)  	r.HandleFunc("/", rootPage).Methods(http.MethodGet)  	r.HandleFunc("/nav", navPage).Methods(http.MethodGet) @@ -834,6 +672,8 @@ func NewHandler(s Service, staticDir string) http.Handler {  	r.HandleFunc("/vote/{id}", vote).Methods(http.MethodPost)  	r.HandleFunc("/follow/{id}", follow).Methods(http.MethodPost)  	r.HandleFunc("/unfollow/{id}", unfollow).Methods(http.MethodPost) +	r.HandleFunc("/accept/{id}", accept).Methods(http.MethodPost) +	r.HandleFunc("/reject/{id}", reject).Methods(http.MethodPost)  	r.HandleFunc("/mute/{id}", mute).Methods(http.MethodPost)  	r.HandleFunc("/unmute/{id}", unMute).Methods(http.MethodPost)  	r.HandleFunc("/block/{id}", block).Methods(http.MethodPost) @@ -852,7 +692,7 @@ func NewHandler(s Service, staticDir string) http.Handler {  	r.HandleFunc("/fluoride/unlike/{id}", fUnlike).Methods(http.MethodPost)  	r.HandleFunc("/fluoride/retweet/{id}", fRetweet).Methods(http.MethodPost)  	r.HandleFunc("/fluoride/unretweet/{id}", fUnretweet).Methods(http.MethodPost) -	r.HandleFunc("/fluoride/status/{id}", statusPopup).Methods(http.MethodGet) +	r.HandleFunc("/fluoride/status/{id}", fStatus).Methods(http.MethodGet)  	r.PathPrefix("/static").Handler(http.StripPrefix("/static",  		http.FileServer(http.Dir(staticDir)))) diff --git a/static/fluoride.js b/static/fluoride.js index 9cf318b..8244d80 100644 --- a/static/fluoride.js +++ b/static/fluoride.js @@ -136,6 +136,11 @@ function isInView(el) {  }  function replyToLinkLocal(a) { +	if (!a) +		return; +	var id = a.getAttribute("href"); +	if (!id || id[0] != "#") +		return;  	a.onmouseenter = function(event) {  		var id = event.target.getAttribute("href");  		var status = document.querySelector(id); @@ -260,8 +265,9 @@ function handleReplyLink(a) {  function handleStatusLink(a) {  	if (a.classList.contains("mention")) -		return; -	a.target = "_blank"; +		a.removeAttribute("target"); +	else +		a.target = "_blank";  }  document.addEventListener("DOMContentLoaded", function() {  @@ -292,6 +298,11 @@ document.addEventListener("DOMContentLoaded", function() {  			handleStatusLink(links[j]);  		}  	} + +	var links = document.querySelectorAll(".user-profile-decription a"); +	for (var j = 0; j < links.length; j++) { +		links[j].target = "_blank"; +	}  });  // @license-end diff --git a/static/style.css b/static/style.css index 095b46e..1921f5e 100644 --- a/static/style.css +++ b/static/style.css @@ -1,11 +1,14 @@ +body { +	background-color: #d2d2d2; +} +  .status-container-container {  	margin: 0 -4px 12px -4px;  	padding: 4px;  	border-left: 4px solid transparent;  } -.status-container-container:target, -.status-container-container.unread { +.status-container-container:target {  	border-color: #777777;  } @@ -173,6 +176,21 @@  	font-size: 13pt;  } +.notification-container { +	margin: 0 -4px 12px -4px; +	padding: 4px; +	border-left: 4px solid transparent; +} + +.notification-container.unread { +	border-color: #777777; +} + +.notification-container.favourite .status-container, +.notification-container.reblog .status-container { +	opacity: 0.6; +} +  .notification-info-text span {  	vertical-align: middle;  } @@ -231,7 +249,7 @@  .user-profile-img-container {  	display: inline-block; -	margin: 0 4px 0 0; +	margin: 0 4px 4px 0;  }  .user-profile-details-container { @@ -281,7 +299,7 @@ a, .btn-link {  a:hover,  .btn-link:hover { -	color: #9899c4; +	color: #8387bf;  }  .status-visibility { @@ -312,7 +330,6 @@ a:hover,  	bottom: 0;  	left: 0;  	right: 0; -	margin-bottom: 5px;  }  .img-link:hover .status-nsfw-overlay { @@ -322,7 +339,7 @@ a:hover,  .status-video-container {  	display: inline-block;  	position: relative; -	margin: 2.5px 0; +	margin-bottom: 5px;  }  .status-video-container:hover .status-nsfw-overlay { @@ -423,7 +440,7 @@ img.emoji {  	margin: 4px 0;  } -.settings-form-field * { +.settings-form-field>* {  	vertical-align: middle;  } @@ -433,7 +450,7 @@ img.emoji {  #reply-popup {  	position: absolute; -	background: #ffffff; +	background-color: #d2d2d2;  	border: 1px solid #aaaaaa;  	padding: 4px 8px;  	z-index: 3; @@ -442,7 +459,7 @@ img.emoji {  #reply-to-popup {  	position: absolute; -	background: #ffffff; +	background-color: #d2d2d2;  	border: 1px solid #aaaaaa;  	padding: 4px 8px;  	z-index: 3; @@ -462,7 +479,7 @@ img.emoji {  .more-content {  	display: none;  	position: absolute; -	background-color: #ffffff; +	background-color: #d2d2d2;  	padding: 2px 4px;  	border: 1px solid #aaaaaa;  } diff --git a/templates/header.tmpl b/templates/header.tmpl index 0c81b72..df2b6af 100644 --- a/templates/header.tmpl +++ b/templates/header.tmpl @@ -14,8 +14,8 @@  	{{if $.Ctx.AntiDopamineMode}}  	<meta name="antidopamine_mode" content="{{$.Ctx.AntiDopamineMode}}">  	{{end}} -	{{if .AutoRefresh}} -	<meta http-equiv="refresh" content="30"> +	{{if .RefreshInterval}} +	<meta http-equiv="refresh" content="{{.RefreshInterval}}">  	{{end}}  	<title> {{if gt .Count 0}}({{.Count}}){{end}} {{.Title}} </title>  	<link rel="stylesheet" href="/static/style.css"> diff --git a/templates/nav.tmpl b/templates/nav.tmpl index f3db9f1..98f0532 100644 --- a/templates/nav.tmpl +++ b/templates/nav.tmpl @@ -8,7 +8,7 @@  	</div>  	<div class="user-info-details-container">  		<div class="user-info-details-name"> -			<span class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </span>   +			<bdi class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </bdi>    			<a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)">  				<span class="status-uname"> @{{.User.Acct}} </span>  			</a> @@ -17,16 +17,18 @@  			<a class="nav-link" href="/timeline/home" accesskey="1" title="Home timeline (1)">home</a>  			<a class="nav-link" href="/timeline/direct" accesskey="2" title="Direct timeline (2)">direct</a>  			<a class="nav-link" href="/timeline/local" accesskey="3" title="Local timeline (3)">local</a> -			<a class="nav-link" href="/timeline/twkn" accesskey="4" title="The Whole Known Netwwork (4)">twkn</a> -			<a class="nav-link" href="/search" accesskey="5" title="Search (5)">search</a> -			<a class="nav-link" href="/about" accesskey="6" title="About (6)">about</a> +			<a class="nav-link" href="/timeline/remote" accesskey="4" title="Remote timeline (4)">remote</a> +			<a class="nav-link" href="/timeline/twkn" accesskey="5" title="The Whole Known Netwwork (5)">twkn</a> +			<a class="nav-link" href="/search" accesskey="6" title="Search (6)">search</a>  		</div>  		<div>  			<a class="nav-link" href="/settings" target="_top" accesskey="7" title="Settings (7)">settings</a>  			<form class="signout" action="/signout" method="post" target="_top">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="signout" class="btn-link nav-link" accesskey="8" title="Signout (8)">  			</form> +			<a class="nav-link" href="/about" accesskey="9" title="About (9)">about</a>  		</div>  	</div>  </div> diff --git a/templates/notification.tmpl b/templates/notification.tmpl index 10eaafc..4eed61b 100644 --- a/templates/notification.tmpl +++ b/templates/notification.tmpl @@ -11,13 +11,14 @@  	{{if .ReadID}}  	<form class="notification-read" action="/notifications/read?max_id={{.ReadID}}" method="post" target="_self">  		<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +		<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  		<input type="submit" value="read" class="btn-link" accesskey="C" title="Clear unread notifications (C)">  	</form>  	{{end}}  </div>  {{range .Notifications}} -<div class="status-container-container {{if .Pleroma}}{{if not .Pleroma.IsSeen}}unread{{end}}{{end}}"> +<div class="notification-container {{.Type}} {{if .Pleroma}}{{if not .Pleroma.IsSeen}}unread{{end}}{{end}}">  	{{if eq .Type "follow"}}  	<div class="notification-follow-container">  		<div class="status-profile-img-container"> @@ -27,7 +28,7 @@  		</div>  		<div class="notification-follow">  			<div class="notification-info-text"> -				<span class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </span>   +				<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>    				<span class="notification-text"> followed you -   					<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>   				</span> @@ -38,6 +39,37 @@  		</div>  	</div> +	{{else if eq .Type "follow_request"}} +	<div class="notification-follow-container"> +		<div class="status-profile-img-container"> +			<a class="img-link" href="/user/{{.Account.ID}}"> +				<img class="status-profile-img" src="{{.Account.AvatarStatic}}" title="@{{.Account.Acct}}" alt="profile-avatar" height="48" /> +			</a> +		</div> +		<div class="notification-follow"> +			<div class="notification-info-text"> +				<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>   +				<span class="notification-text"> wants to follow you -  +					<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>  +				</span> +			</div> +			<div> +				<a href="/user/{{.Account.ID}}"> <span class="status-uname"> @{{.Account.Acct}} </span> </a> +			</div> +			<form class="d-inline" action="/accept/{{.Account.ID}}" method="post" target="_self"> +				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}"> +				<input type="submit" value="accept" class="btn-link"> +			</form> +			- +			<form class="d-inline" action="/reject/{{.Account.ID}}" method="post" target="_self"> +				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}"> +				<input type="submit" value="reject" class="btn-link"> +			</form> +		</div> +	</div> +  	{{else if eq .Type "mention"}}  	{{template "status" (WithContext .Status $.Ctx)}} @@ -68,6 +100,20 @@  		</span>  	</div>  	{{template "status" (WithContext .Status $.Ctx)}} + +	{{else}} +	<div class="retweet-info"> +		<a class="img-link" href="/user/{{.Account.ID}}"> +			<img class="status-profile-img" src="{{.Account.AvatarStatic}}" title="@{{.Account.Acct}}" alt="avatar" height="48" /> +		</a> +		<a href="/user/{{.Account.ID}}"> +			<span class="status-uname"> @{{.Account.Acct}} </span> +		</a> +		<span class="notification-text"> {{.Type}} -  +			<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>  +		</span> +	</div> +	{{if .Status}}{{template "status" (WithContext .Status $.Ctx)}}{{end}}  	{{end}}  </div>  {{end}} diff --git a/templates/postform.tmpl b/templates/postform.tmpl index b38cb9d..b81126c 100644 --- a/templates/postform.tmpl +++ b/templates/postform.tmpl @@ -1,6 +1,7 @@  {{with .Data}}  <form class="post-form" action="/post" method="POST" enctype="multipart/form-data" target="_self">  	<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +	<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  	{{if .ReplyContext}}  	<input type="hidden" name="reply_to_id" value="{{.ReplyContext.InReplyToID}}" />  	<label for="post-content" class="post-form-title"> Reply to {{.ReplyContext.InReplyToName}} </label> @@ -43,6 +44,7 @@  		</span>  	</div>  	<button type="submit" accesskey="P" title="Post (P)"> Post </button> +	<button type="reset" title="Reset"> Reset </button>  </form>  {{end}} diff --git a/templates/requestlist.tmpl b/templates/requestlist.tmpl new file mode 100644 index 0000000..eec75f2 --- /dev/null +++ b/templates/requestlist.tmpl @@ -0,0 +1,36 @@ +{{with .Data}} +<div> +	{{range .}} +	<div class="user-list-item"> +		<div class="user-list-profile-img"> +			<a class="img-link" href="/user/{{.ID}}"> +				<img class="status-profile-img" src="{{.AvatarStatic}}" title="@{{.Acct}}" alt="avatar" height="48" /> +			</a> +		</div> +		<div class="user-list-name"> +			<div> +				<div class="status-dname"> {{EmojiFilter .DisplayName .Emojis}} </div>   +				<a class="img-link" href="/user/{{.ID}}"> +					<div class="status-uname"> @{{.Acct}} </div> +				</a> +			</div> +			<form class="d-inline" action="/accept/{{.ID}}" method="post" target="_self"> +				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}"> +				<input type="submit" value="accept" class="btn-link"> +			</form> +			- +			<form class="d-inline" action="/reject/{{.ID}}" method="post" target="_self"> +				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}"> +				<input type="submit" value="reject" class="btn-link"> +			</form> +		</div> +	</div> +	{{else}} +	<div class="no-data-found">No data found</div> +	{{end}} +</div> +{{else}} +<div class="no-data-found">No data found</div> +{{end}} diff --git a/templates/root.tmpl b/templates/root.tmpl index ef25c90..b1305f5 100644 --- a/templates/root.tmpl +++ b/templates/root.tmpl @@ -6,7 +6,7 @@  	<link rel="icon" type="image/png" href="/static/favicon.png">  	<title>{{.Title}}</title>  </head> -<frameset cols="420px,*"> +<frameset cols="424px,*">  	<frameset rows="316px,*">  		<frame name="nav" src="/nav">   		<frame name="notification" src="/notifications">  diff --git a/templates/settings.tmpl b/templates/settings.tmpl index 972e1a2..4a72b6a 100644 --- a/templates/settings.tmpl +++ b/templates/settings.tmpl @@ -4,6 +4,7 @@  <form id="settings-form" action="/settings" method="POST">  	<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +	<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  	<div class="settings-form-field">  		<label for="visibility"> Default format </label>  		{{$defFormat := .Settings.DefaultFormat}} @@ -23,6 +24,17 @@  		</select>  	</div>  	<div class="settings-form-field"> +		<label for="notification-interval"> Refresh Notifications </label> +		<select id="notification-interval" name="notification_interval"> +			<option value="0" {{if eq .Settings.NotificationInterval 0}}selected{{end}}>Disabled</option> +			<option value="30" {{if eq .Settings.NotificationInterval 30}}selected{{end}}>After 30s</option> +			<option value="60" {{if eq .Settings.NotificationInterval 60}}selected{{end}}>After 1m</option> +			<option value="120" {{if eq .Settings.NotificationInterval 120}}selected{{end}}>After 2m</option> +			<option value="300" {{if eq .Settings.NotificationInterval 300}}selected{{end}}>After 5m</option> +			<option value="600" {{if eq .Settings.NotificationInterval 600}}selected{{end}}>After 10m</option> +		</select> +	</div> +	<div class="settings-form-field">  		<input id="copy-scope" name="copy_scope" type="checkbox" value="true" {{if .Settings.CopyScope}}checked{{end}}>  		<label for="copy-scope"> Copy scope when replying </label>  	</div> @@ -39,10 +51,6 @@  		<label for="mask-nsfw"> Mask NSFW attachments </label>  	</div>  	<div class="settings-form-field"> -		<input id="auto-refresh-notifications" name="auto_refresh_notifications" type="checkbox" value="true" {{if .Settings.AutoRefreshNotifications}}checked{{end}}> -		<label for="auto-refresh-notifications"> Auto refresh notifications </label> -	</div> -	<div class="settings-form-field">  		<input id="fluoride-mode" name="fluoride_mode" type="checkbox" value="true" {{if .Settings.FluorideMode}}checked{{end}}>  		<label for="fluoride-mode"> Enable <abbr title="Enable JavaScript based functionality, e.g., like/retweet without page reload and reply preview on thread page">fluoride mode</abbr> </label>  	</div> diff --git a/templates/status.tmpl b/templates/status.tmpl index 4cdb459..a1e2d9f 100644 --- a/templates/status.tmpl +++ b/templates/status.tmpl @@ -5,7 +5,7 @@  		<a class="img-link" href="/user/{{.Account.ID}}">  			<img class="status-profile-img" src="{{.Account.AvatarStatic}}" title="@{{.Account.Acct}}" alt="avatar" height="24" />  		</a> -		<span class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </span>   +		<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>    		<a href="/user/{{.Account.ID}}">   			<span class="status-uname"> @{{.Account.Acct}} </span>   		</a> @@ -23,7 +23,7 @@  		</div>  		<div class="status">   			<div class="status-name"> -				<span class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </span>  +				<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>   				<a href="/user/{{.Account.ID}}">  					<span class="status-uname"> @{{.Account.Acct}} </span>  				</a> @@ -38,23 +38,27 @@  						{{if .Muted}}  						<form action="/unmuteconv/{{.ID}}" method="post" target="_self">  							<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +							<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  							<input type="submit" value="unmute" class="btn-link more-link">  						</form>  						{{else}}  						<form action="/muteconv/{{.ID}}" method="post" target="_self">  							<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +							<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  							<input type="submit" value="mute" class="btn-link more-link">  						</form>  						{{end}}  						{{if .Bookmarked}}  						<form action="/unbookmark/{{.ID}}" method="post" target="_self">  							<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +							<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  							<input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}">  							<input type="submit" value="unbookmark" class="btn-link more-link">  						</form>  						{{else}}  						<form action="/bookmark/{{.ID}}" method="post" target="_self">  							<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +							<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  							<input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}">  							<input type="submit" value="bookmark" class="btn-link more-link">  						</form> @@ -62,6 +66,7 @@  						{{if eq $.Ctx.UserID .Account.ID}}  						<form action="/delete/{{.ID}}" method="post" target="_self">  							<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +							<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  							<input type="submit" value="delete" class="btn-link more-link">  						</form>  						{{end}} @@ -91,7 +96,9 @@  				{{if eq .Type "image"}}  				{{if $.Ctx.HideAttachments}} -				<a href="{{.URL}}" target="_blank" title="{{.Description}}"> [image] </a> +				<a href="{{.URL}}" target="_blank"> +					{{if .Description}}[{{.Description}}]{{else}}[image]{{end}} +				</a>  				{{else}}  				<a class="img-link" href="{{.URL}}" target="_blank" title="{{.Description}}">  					<img class="status-image" src="{{.URL}}" alt="status-image" height="240" /> @@ -103,7 +110,9 @@  				{{else if eq .Type "audio"}}  				{{if $.Ctx.HideAttachments}} -				<a href="{{.URL}}" target="_blank" title="{{.Description}}"> [audio] </a> +				<a href="{{.URL}}" target="_blank"> +					{{if .Description}}[{{.Description}}]{{else}}[audio]{{end}} +				</a>  				{{else}}  				<audio class="status-audio" controls title="{{.Description}}">  					<source src="{{.URL}}"> @@ -113,7 +122,9 @@  				{{else if eq .Type "video"}}  				{{if $.Ctx.HideAttachments}} -				<a href="{{.URL}}" target="_blank" title="{{.Description}}"> [video] </a> +				<a href="{{.URL}}" target="_blank"> +					{{if .Description}}[{{.Description}}]{{else}}[video]{{end}} +				</a>  				{{else}}  				<div class="status-video-container" title="{{.Description}}">  					<video class="status-video" controls height="240"> @@ -127,7 +138,9 @@  				{{end}}  				{{else}} -				<a href="{{.URL}}" target="_blank" title="{{.Description}}"> [attachment] </a> +				<a href="{{.URL}}" target="_blank">  +					{{if .Description}}[{{.Description}}]{{else}}[attachment]{{end}} +				</a>  				{{end}}  				{{end}}  			</div> @@ -135,16 +148,17 @@  			{{if .Poll}}  			<form class="poll-form" action="/vote/{{.Poll.ID}}" method="POST" target="_self">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="hidden" name="status_id" value="{{$s.ID}}">  				{{range $i, $o := .Poll.Options}}  				<div class="poll-option">  					{{if (or $s.Poll.Expired $s.Poll.Voted)}} -					<div> {{$o.Title}} - {{$o.VotesCount}} votes </div> +					<div> {{EmojiFilter $o.Title $s.Emojis}} - {{$o.VotesCount}} votes </div>  					{{else}}  					<input type="{{if $s.Poll.Multiple}}checkbox{{else}}radio{{end}}" name="choices"   						id="poll-{{$s.ID}}-{{$i}}" value="{{$i}}">  					<label for="poll-{{$s.ID}}-{{$i}}">  -						{{$o.Title}}  +						{{EmojiFilter $o.Title $s.Emojis}}   					</label>  					{{end}}  				</div> @@ -187,6 +201,7 @@  					{{$rt := "retweet"}} {{if .Reblogged}} {{$rt = "unretweet"}} {{end}}  					<form class="status-retweet" data-action="{{$rt}}" action="/{{$rt}}/{{.ID}}" method="post" target="_self">  						<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +						<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  						<input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}">  						<input type="submit" value="{{$rt}}" class="btn-link">  						<a class="status-retweet-count" href="/retweetedby/{{.ID}}" title="click to see the the list">  @@ -201,6 +216,7 @@  					{{$like := "like"}} {{if .Favourited}} {{$like = "unlike"}} {{end}}  					<form class="status-like" data-action="{{$like}}" action="/{{$like}}/{{.ID}}" method="post" target="_self">  						<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +						<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  						<input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}">  						<input type="submit" value="{{$like}}" class="btn-link">  						<a class="status-like-count" href="/likedby/{{.ID}}" title="click to see the the list">  diff --git a/templates/timeline.tmpl b/templates/timeline.tmpl index eabb3ed..bde050a 100644 --- a/templates/timeline.tmpl +++ b/templates/timeline.tmpl @@ -2,6 +2,16 @@  {{template "header.tmpl" (WithContext .CommonData $.Ctx)}}  <div class="page-title"> {{.Title}} </div> +{{if eq .Type "remote"}} +<form class="search-form" action="/timeline/remote" method="GET"> +	<span class="post-form-field"> +		<label for="instance"> Instance </label> +		<input id="instance" name="instance" value="{{.Instance}}"> +	</span> +	<button type="submit"> Submit </button> +</form> +{{end}} +  {{range .Statuses}}  {{template "status.tmpl" (WithContext . $.Ctx)}}  {{end}} diff --git a/templates/user.tmpl b/templates/user.tmpl index b7a12b9..af6a8d1 100644 --- a/templates/user.tmpl +++ b/templates/user.tmpl @@ -11,7 +11,7 @@  	</div>  	<div class="user-profile-details-container">  		<div> -			<span class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </span>   +			<bdi class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </bdi>    			<span class="status-uname"> @{{.User.Acct}} </span>  			<a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile">  				source @@ -23,11 +23,13 @@  			{{if .User.Pleroma.Relationship.Following}}   			<form class="d-inline" action="/unfollow/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="unfollow" class="btn-link">  			</form>  			{{else}}  			<form class="d-inline" action="/follow/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="{{if .User.Pleroma.Relationship.Requested}}resend request{{else}}follow{{end}}" class="btn-link">  			</form>  			{{end}} @@ -35,6 +37,7 @@  			-  			<form class="d-inline" action="/unfollow/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="cancel request" class="btn-link">  			</form>  			{{end}} @@ -42,11 +45,13 @@  			{{if .User.Pleroma.Relationship.Subscribing}}  			<form class="d-inline" action="/unsubscribe/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="unsubscribe" class="btn-link">  			</form>  			{{else}}  			<form class="d-inline" action="/subscribe/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="subscribe" class="btn-link">  			</form>  			{{end}} @@ -55,11 +60,13 @@  			{{if .User.Pleroma.Relationship.Blocking}}  			<form class="d-inline" action="/unblock/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="unblock" class="btn-link">  			</form>  			{{else}}  			<form class="d-inline" action="/block/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="block" class="btn-link">  			</form>  			{{end}} @@ -67,11 +74,13 @@  			{{if .User.Pleroma.Relationship.Muting}}  			<form class="d-inline" action="/unmute/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="unmute" class="btn-link">  			</form>  			{{else}}  			<form class="d-inline" action="/mute/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="mute" class="btn-link">  			</form>  			{{end}} @@ -80,11 +89,13 @@  			{{if .User.Pleroma.Relationship.ShowingReblogs}}  			<form class="d-inline" action="/follow/{{.User.ID}}?reblogs=false" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="hide retweets" class="btn-link">  			</form>  			{{else}}  			<form class="d-inline" action="/follow/{{.User.ID}}" method="post">  				<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}"> +				<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">  				<input type="submit" value="show retweets" class="btn-link">  			</form>  			{{end}} @@ -99,10 +110,11 @@  		</div>  		{{if .IsCurrent}}  		<div> -			<a href="/user/{{.User.ID}}/bookmarks"> bookmarks </a> - -			<a href="/user/{{.User.ID}}/likes"> likes </a> - -			<a href="/user/{{.User.ID}}/mutes"> mutes </a> - -			<a href="/user/{{.User.ID}}/blocks"> blocks </a> +			<a href="/user/{{.User.ID}}/bookmarks"> bookmarks </a> +			- <a href="/user/{{.User.ID}}/likes"> likes </a> +			- <a href="/user/{{.User.ID}}/mutes"> mutes </a> +			- <a href="/user/{{.User.ID}}/blocks"> blocks </a> +			{{if .User.Locked}}- <a href="/user/{{.User.ID}}/requests"> requests </a>{{end}}  		</div>  		{{end}}  		<div> @@ -162,6 +174,10 @@  {{else if eq .Type "blocks"}}  <div class="page-title"> Blocks </div>  {{template "userlist.tmpl" (WithContext .Users $.Ctx)}} + +{{else if eq .Type "requests"}} +<div class="page-title"> Follow requests </div> +{{template "requestlist.tmpl" (WithContext .Users $.Ctx)}}  {{end}}  <div class="pagination"> @@ -1,4 +1,4 @@ -package kv +package util  import (  	"errors" | 
