diff --git a/.gitignore b/.gitignore index 037bea6..516f77b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ bloat database -bloat.def.conf diff --git a/INSTALL b/INSTALL index 032f612..c73fd78 100644 --- a/INSTALL +++ b/INSTALL @@ -15,12 +15,12 @@ This will perform a system wide installation of bloat. By default, it will install the binary in /usr/local/bin and data files in /usr/local/share/bloat. You can change these paths by editing the Makefile. -3. Edit and copy the config file -Edit the generated config file to you liking and then copy it to the default -config location. Comments in the config file describe what each config value -does. For most cases, you only need to change the value of "client_website". -$ $EDITOR bloat.def.conf -# cp bloat.def.conf /etc/bloat.conf +3. Edit the config file +bloat looks for a file named bloat.conf in the working directory and +/etc/bloat in that order. You can also specify another file by using the -f +flag. Comments in the config file describe what each config value does. For +most cases, you only need to change the value of "client_website". +# $EDITOR /etc/bloat.conf 4. Create database directory Create a directory to store session information. Optionally, create a user diff --git a/Makefile b/Makefile index 4231015..1b32268 100644 --- a/Makefile +++ b/Makefile @@ -14,17 +14,11 @@ SRC=main.go \ service/*.go \ util/*.go \ -all: bloat bloat.def.conf +all: bloat bloat: $(SRC) $(TMPL) $(GO) build $(GOFLAGS) -o bloat main.go -bloat.def.conf: - sed -e "s%=database%=/var/bloat%g" \ - -e "s%=templates%=$(SHAREPATH)/templates%g" \ - -e "s%=static%=$(SHAREPATH)/static%g" \ - < bloat.conf > bloat.def.conf - install: bloat mkdir -p $(DESTDIR)$(BINPATH) \ $(DESTDIR)$(SHAREPATH)/templates \ @@ -35,6 +29,10 @@ install: bloat chmod 0644 $(DESTDIR)$(SHAREPATH)/templates/* cp -r static/* $(DESTDIR)$(SHAREPATH)/static chmod 0644 $(DESTDIR)$(SHAREPATH)/static/* + sed -e "s%=database%=/var/bloat%g" \ + -e "s%=templates%=$(SHAREPATH)/templates%g" \ + -e "s%=static%=$(SHAREPATH)/static%g" \ + < bloat.conf > /etc/bloat.conf uninstall: rm -f $(DESTDIR)$(BINPATH)/bloat @@ -42,4 +40,3 @@ uninstall: clean: rm -f bloat - rm -f bloat.def.conf diff --git a/README b/README index 9c76da1..b00592f 100644 --- a/README +++ b/README @@ -15,11 +15,11 @@ Building and Installation: Typing make will build the binary $ make -Edit the provided config file. See the bloat.conf file for more details. +Edit the default config file. See the bloat.conf file for more details. $ ed bloat.conf Run the binary -$ ./bloat -f bloat.conf +$ ./bloat You can now access the frontend at http://127.0.0.1:8080, which is the default listen address. See the INSTALL file for more details. diff --git a/config/config.go b/config/config.go index 8678f52..bbb327c 100644 --- a/config/config.go +++ b/config/config.go @@ -108,21 +108,30 @@ func Parse(r io.Reader) (c *config, err error) { return } -func ParseFile(file string) (c *config, err error) { - f, err := os.Open(file) - if err != nil { - return +func ParseFiles(files []string) (c *config, err error) { + var lastErr error + for _, file := range files { + f, err := os.Open(file) + if err != nil { + lastErr = err + if os.IsNotExist(err) { + continue + } + return nil, err + } + defer f.Close() + info, err := f.Stat() + if err != nil { + lastErr = err + return nil, err + } + if info.IsDir() { + continue + } + return Parse(f) } - defer f.Close() - - info, err := f.Stat() - if err != nil { - return + if lastErr == nil { + lastErr = errors.New("invalid config file") } - - if info.IsDir() { - return nil, errors.New("invalid config file") - } - - return Parse(f) + return nil, lastErr } diff --git a/main.go b/main.go index cac5eee..3b5ccba 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "errors" + "flag" "fmt" "log" "net/http" @@ -17,7 +18,7 @@ import ( ) var ( - configFile = "/etc/bloat.conf" + configFiles = []string{"bloat.conf", "/etc/bloat.conf"} ) func errExit(err error) { @@ -26,19 +27,13 @@ func errExit(err error) { } func main() { - opts, _, err := util.Getopts(os.Args, "f:") - if err != nil { - errExit(err) - } + configFile := flag.String("f", "", "config file") + flag.Parse() - for _, opt := range opts { - switch opt.Option { - case 'f': - configFile = opt.Value - } + if len(*configFile) > 0 { + configFiles = []string{*configFile} } - - config, err := config.ParseFile(configFile) + config, err := config.ParseFiles(configFiles) if err != nil { errExit(err) } diff --git a/mastodon/accounts.go b/mastodon/accounts.go index 694e672..df0a3b7 100644 --- a/mastodon/accounts.go +++ b/mastodon/accounts.go @@ -243,9 +243,13 @@ func (c *Client) AccountUnblock(ctx context.Context, id string) (*Relationship, } // AccountMute mute the account. -func (c *Client) AccountMute(ctx context.Context, id string) (*Relationship, error) { +func (c *Client) AccountMute(ctx context.Context, id string, notifications *bool) (*Relationship, error) { + params := url.Values{} + if notifications != nil { + params.Set("notifications", strconv.FormatBool(*notifications)) + } var relationship Relationship - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), nil, &relationship, nil) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), params, &relationship, nil) if err != nil { return nil, err } diff --git a/mastodon/helper.go b/mastodon/helper.go index 05af20f..cb0013d 100644 --- a/mastodon/helper.go +++ b/mastodon/helper.go @@ -3,12 +3,28 @@ package mastodon import ( "encoding/base64" "encoding/json" - "errors" "fmt" "net/http" "os" ) +type Error struct { + code int + err string +} + +func (e Error) Error() string { + return e.err +} + +func (e Error) IsAuthError() bool { + switch e.code { + case http.StatusForbidden, http.StatusUnauthorized: + return true + } + return false +} + // Base64EncodeFileName returns the base64 data URI format string of the file with the file name. func Base64EncodeFileName(filename string) (string, error) { file, err := os.Open(filename) @@ -51,5 +67,8 @@ func parseAPIError(prefix string, resp *http.Response) error { errMsg = fmt.Sprintf("%s: %s", errMsg, e.Error) } - return errors.New(errMsg) + return Error{ + code: resp.StatusCode, + err: errMsg, + } } diff --git a/mastodon/notification.go b/mastodon/notification.go index 656e6a1..e94f901 100644 --- a/mastodon/notification.go +++ b/mastodon/notification.go @@ -23,9 +23,12 @@ type Notification struct { } // GetNotifications return notifications. -func (c *Client) GetNotifications(ctx context.Context, pg *Pagination, excludes []string) ([]*Notification, error) { +func (c *Client) GetNotifications(ctx context.Context, pg *Pagination, includes, excludes []string) ([]*Notification, error) { var notifications []*Notification params := url.Values{} + for _, include := range includes { + params.Add("include_types[]", include) + } for _, exclude := range excludes { params.Add("exclude_types[]", exclude) } diff --git a/mastodon/status.go b/mastodon/status.go index 80e7e0e..8b148b3 100644 --- a/mastodon/status.go +++ b/mastodon/status.go @@ -19,6 +19,19 @@ type ReplyInfo struct { Number int `json:"number"` } +type CreatedAt struct { + time.Time +} + +func (t *CreatedAt) UnmarshalJSON(d []byte) error { + // Special case to handle retweets from GNU Social + // which returns empty string ("") in created_at + if len(d) == 2 && string(d) == `""` { + return nil + } + return t.Time.UnmarshalJSON(d) +} + // Status is struct to hold status. type Status struct { ID string `json:"id"` @@ -29,7 +42,7 @@ type Status struct { InReplyToAccountID interface{} `json:"in_reply_to_account_id"` Reblog *Status `json:"reblog"` Content string `json:"content"` - CreatedAt time.Time `json:"created_at"` + CreatedAt CreatedAt `json:"created_at"` Emojis []Emoji `json:"emojis"` RepliesCount int64 `json:"replies_count"` ReblogsCount int64 `json:"reblogs_count"` diff --git a/model/settings.go b/model/settings.go index c4e8aec..1f83c75 100644 --- a/model/settings.go +++ b/model/settings.go @@ -1,31 +1,33 @@ 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"` - NotificationInterval int `json:"notifications_interval"` - FluorideMode bool `json:"fluoride_mode"` - DarkMode bool `json:"dark_mode"` - AntiDopamineMode bool `json:"anti_dopamine_mode"` - CSS string `json:"css"` + 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"` + HideUnsupportedNotifs bool `json:"hide_unsupported_notifs"` + CSS string `json:"css"` } func NewSettings() *Settings { return &Settings{ - DefaultVisibility: "public", - DefaultFormat: "", - CopyScope: true, - ThreadInNewTab: false, - HideAttachments: false, - MaskNSFW: true, - NotificationInterval: 0, - FluorideMode: false, - DarkMode: false, - AntiDopamineMode: false, - CSS: "", + DefaultVisibility: "public", + DefaultFormat: "", + CopyScope: true, + ThreadInNewTab: false, + HideAttachments: false, + MaskNSFW: true, + NotificationInterval: 0, + FluorideMode: false, + DarkMode: false, + AntiDopamineMode: false, + HideUnsupportedNotifs: false, + CSS: "", } } diff --git a/renderer/renderer.go b/renderer/renderer.go index 6c9877a..f50e185 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -1,8 +1,8 @@ package renderer import ( - "fmt" "io" + "regexp" "strconv" "strings" "text/template" @@ -39,29 +39,28 @@ type TemplateData struct { Ctx *Context } +func emojiHTML(e mastodon.Emoji, height string) string { + return `:` + e.ShortCode + `:` +} + func emojiFilter(content string, emojis []mastodon.Emoji) string { var replacements []string - var r string for _, e := range emojis { - r = fmt.Sprintf("\":%s:\"", - e.URL, e.ShortCode, e.ShortCode) - replacements = append(replacements, ":"+e.ShortCode+":", r) + replacements = append(replacements, ":"+e.ShortCode+":", emojiHTML(e, "24")) } return strings.NewReplacer(replacements...).Replace(content) } -func statusContentFilter(spoiler string, content string, - emojis []mastodon.Emoji, mentions []mastodon.Mention) string { +var quoteRE = regexp.MustCompile("(?mU)(^|> *|\n)(>.*)( 0 { - content = spoiler + "
" + content + content = spoiler + "
" + content } + content = quoteRE.ReplaceAllString(content, `$1$2$3`) + var replacements []string for _, e := range emojis { - r = fmt.Sprintf("\":%s:\"", - e.URL, e.ShortCode, e.ShortCode) - replacements = append(replacements, ":"+e.ShortCode+":", r) + replacements = append(replacements, ":"+e.ShortCode+":", emojiHTML(e, "32")) } for _, m := range mentions { replacements = append(replacements, `"`+m.URL+`"`, `"/user/`+m.ID+`" title="@`+m.Acct+`"`) diff --git a/service/service.go b/service/service.go index a846322..c56114c 100644 --- a/service/service.go +++ b/service/service.go @@ -114,7 +114,8 @@ func (s *service) ErrorPage(c *client, err error, retry bool) error { var sessionErr bool if err != nil { errStr = err.Error() - if err == errInvalidSession || err == errInvalidCSRFToken { + if me, ok := err.(mastodon.Error); ok && me.IsAuthError() || + err == errInvalidSession || err == errInvalidCSRFToken { sessionErr = true } } @@ -417,18 +418,24 @@ func (s *service) NotificationPage(c *client, maxID string, var nextLink string var unreadCount int var readID string - var excludes []string + var includes, excludes []string var pg = mastodon.Pagination{ MaxID: maxID, MinID: minID, Limit: 20, } + if c.s.Settings.HideUnsupportedNotifs { + // Explicitly include the supported types. + // For now, only Pleroma supports this option, Mastadon + // will simply ignore the unknown params. + includes = []string{"follow", "follow_request", "mention", "reblog", "favourite"} + } if c.s.Settings.AntiDopamineMode { - excludes = []string{"follow", "favourite", "reblog"} + excludes = append(excludes, "follow", "favourite", "reblog") } - notifications, err := c.GetNotifications(c.ctx, &pg, excludes) + notifications, err := c.GetNotifications(c.ctx, &pg, includes, excludes) if err != nil { return } @@ -914,8 +921,8 @@ func (s *service) Reject(c *client, id string) (err error) { return c.FollowRequestReject(c.ctx, id) } -func (s *service) Mute(c *client, id string) (err error) { - _, err = c.AccountMute(c.ctx, id) +func (s *service) Mute(c *client, id string, notifications *bool) (err error) { + _, err = c.AccountMute(c.ctx, id, notifications) return } diff --git a/service/transport.go b/service/transport.go index a022b02..02e6106 100644 --- a/service/transport.go +++ b/service/transport.go @@ -415,7 +415,13 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler { mute := handle(func(c *client) error { id, _ := mux.Vars(c.r)["id"] - err := s.Mute(c, id) + q := c.r.URL.Query() + var notifications *bool + if r, ok := q["notifications"]; ok && len(r) > 0 { + notifications = new(bool) + *notifications = r[0] == "true" + } + err := s.Mute(c, id, notifications) if err != nil { return err } @@ -484,20 +490,22 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler { fluorideMode := c.r.FormValue("fluoride_mode") == "true" darkMode := c.r.FormValue("dark_mode") == "true" antiDopamineMode := c.r.FormValue("anti_dopamine_mode") == "true" + hideUnsupportedNotifs := c.r.FormValue("hide_unsupported_notifs") == "true" css := c.r.FormValue("css") settings := &model.Settings{ - DefaultVisibility: visibility, - DefaultFormat: format, - CopyScope: copyScope, - ThreadInNewTab: threadInNewTab, - HideAttachments: hideAttachments, - MaskNSFW: maskNSFW, - NotificationInterval: ni, - FluorideMode: fluorideMode, - DarkMode: darkMode, - AntiDopamineMode: antiDopamineMode, - CSS: css, + DefaultVisibility: visibility, + DefaultFormat: format, + CopyScope: copyScope, + ThreadInNewTab: threadInNewTab, + HideAttachments: hideAttachments, + MaskNSFW: maskNSFW, + NotificationInterval: ni, + FluorideMode: fluorideMode, + DarkMode: darkMode, + AntiDopamineMode: antiDopamineMode, + HideUnsupportedNotifs: hideUnsupportedNotifs, + CSS: css, } err := s.SaveSettings(c, settings) diff --git a/static/fluoride.js b/static/fluoride.js index c7f3109..279483f 100644 --- a/static/fluoride.js +++ b/static/fluoride.js @@ -298,20 +298,24 @@ function setPos(el, cx, cy, mw, mh) { } var imgPrev = null; +var imgX = 0; +var imgY = 0; function handleImgPreview(a) { a.onmouseenter = function(e) { var mw = document.documentElement.clientWidth; var mh = document.documentElement.clientHeight - 24; + imgX = e.clientX; + imgY = e.clientY; var img = document.createElement("img"); img.id = "img-preview"; img.src = e.target.getAttribute("href"); img.style["max-width"] = mw + "px"; img.style["max-height"] = mh + "px"; + imgPrev = img; img.onload = function(e2) { - setPos(e2.target, e.clientX, e.clientY, mw, mh); + setPos(imgPrev, imgX, imgY, mw, mh); } document.body.appendChild(img); - imgPrev = img; } a.onmouseleave = function(e) { var img = document.getElementById("img-preview"); @@ -324,7 +328,9 @@ function handleImgPreview(a) { return; var mw = document.documentElement.clientWidth; var mh = document.documentElement.clientHeight - 24; - setPos(imgPrev, e.clientX, e.clientY, mw, mh); + imgX = e.clientX; + imgY = e.clientY; + setPos(imgPrev, imgX, imgY, mw, mh); } } diff --git a/static/style.css b/static/style.css index cd7e98c..4e2a196 100644 --- a/static/style.css +++ b/static/style.css @@ -517,18 +517,18 @@ img.emoji { margin-top: 6px; } -.notification-title-container { +.page-title-container { margin: 8px 0; } +.page-refresh { + margin-right: 8px; +} + .notification-text { vertical-align: middle; } -.notification-refresh { - margin-right: 8px; -} - .notification-read { display: inline-block; } @@ -575,6 +575,10 @@ kbd { position: fixed; } +.quote { + color: #789922; +} + .dark { background-color: #222222; background-image: none; diff --git a/templates/about.tmpl b/templates/about.tmpl index c0b8418..54316cf 100644 --- a/templates/about.tmpl +++ b/templates/about.tmpl @@ -94,7 +94,7 @@ C - Refresh thread page + Refresh timeline/thread page T diff --git a/templates/nav.tmpl b/templates/nav.tmpl index cbf65c9..ea18a5f 100644 --- a/templates/nav.tmpl +++ b/templates/nav.tmpl @@ -8,7 +8,7 @@