Merge branch 'master' into absolute_fluoride

This commit is contained in:
r 2022-01-27 12:05:15 +00:00
commit b8c0133bcd
29 changed files with 214 additions and 263 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
bloat bloat
database database
bloat.def.conf

12
INSTALL
View File

@ -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. install the binary in /usr/local/bin and data files in /usr/local/share/bloat.
You can change these paths by editing the Makefile. You can change these paths by editing the Makefile.
3. Edit and copy the config file 3. Edit the config file
Edit the generated config file to you liking and then copy it to the default bloat looks for a file named bloat.conf in the working directory and
config location. Comments in the config file describe what each config value /etc/bloat in that order. You can also specify another file by using the -f
does. For most cases, you only need to change the value of "client_website". flag. Comments in the config file describe what each config value does. For
$ $EDITOR bloat.def.conf most cases, you only need to change the value of "client_website".
# cp bloat.def.conf /etc/bloat.conf # $EDITOR /etc/bloat.conf
4. Create database directory 4. Create database directory
Create a directory to store session information. Optionally, create a user Create a directory to store session information. Optionally, create a user

View File

@ -14,17 +14,11 @@ SRC=main.go \
service/*.go \ service/*.go \
util/*.go \ util/*.go \
all: bloat bloat.def.conf all: bloat
bloat: $(SRC) $(TMPL) bloat: $(SRC) $(TMPL)
$(GO) build $(GOFLAGS) -o bloat main.go $(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 install: bloat
mkdir -p $(DESTDIR)$(BINPATH) \ mkdir -p $(DESTDIR)$(BINPATH) \
$(DESTDIR)$(SHAREPATH)/templates \ $(DESTDIR)$(SHAREPATH)/templates \
@ -35,6 +29,10 @@ install: bloat
chmod 0644 $(DESTDIR)$(SHAREPATH)/templates/* chmod 0644 $(DESTDIR)$(SHAREPATH)/templates/*
cp -r static/* $(DESTDIR)$(SHAREPATH)/static cp -r static/* $(DESTDIR)$(SHAREPATH)/static
chmod 0644 $(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: uninstall:
rm -f $(DESTDIR)$(BINPATH)/bloat rm -f $(DESTDIR)$(BINPATH)/bloat
@ -42,4 +40,3 @@ uninstall:
clean: clean:
rm -f bloat rm -f bloat
rm -f bloat.def.conf

4
README
View File

@ -15,11 +15,11 @@ Building and Installation:
Typing make will build the binary Typing make will build the binary
$ make $ 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 $ ed bloat.conf
Run the binary 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 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. listen address. See the INSTALL file for more details.

View File

@ -108,21 +108,30 @@ func Parse(r io.Reader) (c *config, err error) {
return return
} }
func ParseFile(file string) (c *config, err error) { func ParseFiles(files []string) (c *config, err error) {
var lastErr error
for _, file := range files {
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return lastErr = err
if os.IsNotExist(err) {
continue
}
return nil, err
} }
defer f.Close() defer f.Close()
info, err := f.Stat() info, err := f.Stat()
if err != nil { if err != nil {
return lastErr = err
return nil, err
} }
if info.IsDir() { if info.IsDir() {
return nil, errors.New("invalid config file") continue
} }
return Parse(f) return Parse(f)
}
if lastErr == nil {
lastErr = errors.New("invalid config file")
}
return nil, lastErr
} }

19
main.go
View File

@ -2,6 +2,7 @@ package main
import ( import (
"errors" "errors"
"flag"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@ -17,7 +18,7 @@ import (
) )
var ( var (
configFile = "/etc/bloat.conf" configFiles = []string{"bloat.conf", "/etc/bloat.conf"}
) )
func errExit(err error) { func errExit(err error) {
@ -26,19 +27,13 @@ func errExit(err error) {
} }
func main() { func main() {
opts, _, err := util.Getopts(os.Args, "f:") configFile := flag.String("f", "", "config file")
if err != nil { flag.Parse()
errExit(err)
}
for _, opt := range opts { if len(*configFile) > 0 {
switch opt.Option { configFiles = []string{*configFile}
case 'f':
configFile = opt.Value
} }
} config, err := config.ParseFiles(configFiles)
config, err := config.ParseFile(configFile)
if err != nil { if err != nil {
errExit(err) errExit(err)
} }

View File

@ -243,9 +243,13 @@ func (c *Client) AccountUnblock(ctx context.Context, id string) (*Relationship,
} }
// AccountMute mute the account. // 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 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -3,12 +3,28 @@ package mastodon
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "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. // Base64EncodeFileName returns the base64 data URI format string of the file with the file name.
func Base64EncodeFileName(filename string) (string, error) { func Base64EncodeFileName(filename string) (string, error) {
file, err := os.Open(filename) 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) errMsg = fmt.Sprintf("%s: %s", errMsg, e.Error)
} }
return errors.New(errMsg) return Error{
code: resp.StatusCode,
err: errMsg,
}
} }

View File

@ -23,9 +23,12 @@ type Notification struct {
} }
// GetNotifications return notifications. // 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 var notifications []*Notification
params := url.Values{} params := url.Values{}
for _, include := range includes {
params.Add("include_types[]", include)
}
for _, exclude := range excludes { for _, exclude := range excludes {
params.Add("exclude_types[]", exclude) params.Add("exclude_types[]", exclude)
} }

View File

@ -19,6 +19,19 @@ type ReplyInfo struct {
Number int `json:"number"` 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. // Status is struct to hold status.
type Status struct { type Status struct {
ID string `json:"id"` ID string `json:"id"`
@ -29,7 +42,7 @@ type Status struct {
InReplyToAccountID interface{} `json:"in_reply_to_account_id"` InReplyToAccountID interface{} `json:"in_reply_to_account_id"`
Reblog *Status `json:"reblog"` Reblog *Status `json:"reblog"`
Content string `json:"content"` Content string `json:"content"`
CreatedAt time.Time `json:"created_at"` CreatedAt CreatedAt `json:"created_at"`
Emojis []Emoji `json:"emojis"` Emojis []Emoji `json:"emojis"`
RepliesCount int64 `json:"replies_count"` RepliesCount int64 `json:"replies_count"`
ReblogsCount int64 `json:"reblogs_count"` ReblogsCount int64 `json:"reblogs_count"`

View File

@ -11,6 +11,7 @@ type Settings struct {
FluorideMode bool `json:"fluoride_mode"` FluorideMode bool `json:"fluoride_mode"`
DarkMode bool `json:"dark_mode"` DarkMode bool `json:"dark_mode"`
AntiDopamineMode bool `json:"anti_dopamine_mode"` AntiDopamineMode bool `json:"anti_dopamine_mode"`
HideUnsupportedNotifs bool `json:"hide_unsupported_notifs"`
CSS string `json:"css"` CSS string `json:"css"`
} }
@ -26,6 +27,7 @@ func NewSettings() *Settings {
FluorideMode: false, FluorideMode: false,
DarkMode: false, DarkMode: false,
AntiDopamineMode: false, AntiDopamineMode: false,
HideUnsupportedNotifs: false,
CSS: "", CSS: "",
} }
} }

View File

@ -1,8 +1,8 @@
package renderer package renderer
import ( import (
"fmt"
"io" "io"
"regexp"
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
@ -39,29 +39,28 @@ type TemplateData struct {
Ctx *Context Ctx *Context
} }
func emojiHTML(e mastodon.Emoji, height string) string {
return `<img class="emoji" src="` + e.URL + `" alt=":` + e.ShortCode + `:" title=":` + e.ShortCode + `:" height="` + height + `"/>`
}
func emojiFilter(content string, emojis []mastodon.Emoji) string { func emojiFilter(content string, emojis []mastodon.Emoji) string {
var replacements []string var replacements []string
var r string
for _, e := range emojis { for _, e := range emojis {
r = fmt.Sprintf("<img class=\"emoji\" src=\"%s\" alt=\":%s:\" title=\":%s:\" height=\"24\" />", replacements = append(replacements, ":"+e.ShortCode+":", emojiHTML(e, "24"))
e.URL, e.ShortCode, e.ShortCode)
replacements = append(replacements, ":"+e.ShortCode+":", r)
} }
return strings.NewReplacer(replacements...).Replace(content) return strings.NewReplacer(replacements...).Replace(content)
} }
func statusContentFilter(spoiler string, content string, var quoteRE = regexp.MustCompile("(?mU)(^|> *|\n)(&gt;.*)(<br|$)")
emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
var replacements []string func statusContentFilter(spoiler, content string, emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
var r string
if len(spoiler) > 0 { if len(spoiler) > 0 {
content = spoiler + "<br />" + content content = spoiler + "<br/>" + content
} }
content = quoteRE.ReplaceAllString(content, `$1<span class="quote">$2</span>$3`)
var replacements []string
for _, e := range emojis { for _, e := range emojis {
r = fmt.Sprintf("<img class=\"emoji\" src=\"%s\" alt=\":%s:\" title=\":%s:\" height=\"32\" />", replacements = append(replacements, ":"+e.ShortCode+":", emojiHTML(e, "32"))
e.URL, e.ShortCode, e.ShortCode)
replacements = append(replacements, ":"+e.ShortCode+":", r)
} }
for _, m := range mentions { for _, m := range mentions {
replacements = append(replacements, `"`+m.URL+`"`, `"/user/`+m.ID+`" title="@`+m.Acct+`"`) replacements = append(replacements, `"`+m.URL+`"`, `"/user/`+m.ID+`" title="@`+m.Acct+`"`)

View File

@ -114,7 +114,8 @@ func (s *service) ErrorPage(c *client, err error, retry bool) error {
var sessionErr bool var sessionErr bool
if err != nil { if err != nil {
errStr = err.Error() errStr = err.Error()
if err == errInvalidSession || err == errInvalidCSRFToken { if me, ok := err.(mastodon.Error); ok && me.IsAuthError() ||
err == errInvalidSession || err == errInvalidCSRFToken {
sessionErr = true sessionErr = true
} }
} }
@ -417,18 +418,24 @@ func (s *service) NotificationPage(c *client, maxID string,
var nextLink string var nextLink string
var unreadCount int var unreadCount int
var readID string var readID string
var excludes []string var includes, excludes []string
var pg = mastodon.Pagination{ var pg = mastodon.Pagination{
MaxID: maxID, MaxID: maxID,
MinID: minID, MinID: minID,
Limit: 20, 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 { 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 { if err != nil {
return return
} }
@ -914,8 +921,8 @@ func (s *service) Reject(c *client, id string) (err error) {
return c.FollowRequestReject(c.ctx, id) return c.FollowRequestReject(c.ctx, id)
} }
func (s *service) Mute(c *client, id string) (err error) { func (s *service) Mute(c *client, id string, notifications *bool) (err error) {
_, err = c.AccountMute(c.ctx, id) _, err = c.AccountMute(c.ctx, id, notifications)
return return
} }

View File

@ -415,7 +415,13 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
mute := handle(func(c *client) error { mute := handle(func(c *client) error {
id, _ := mux.Vars(c.r)["id"] 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 { if err != nil {
return err return err
} }
@ -484,6 +490,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
fluorideMode := c.r.FormValue("fluoride_mode") == "true" fluorideMode := c.r.FormValue("fluoride_mode") == "true"
darkMode := c.r.FormValue("dark_mode") == "true" darkMode := c.r.FormValue("dark_mode") == "true"
antiDopamineMode := c.r.FormValue("anti_dopamine_mode") == "true" antiDopamineMode := c.r.FormValue("anti_dopamine_mode") == "true"
hideUnsupportedNotifs := c.r.FormValue("hide_unsupported_notifs") == "true"
css := c.r.FormValue("css") css := c.r.FormValue("css")
settings := &model.Settings{ settings := &model.Settings{
@ -497,6 +504,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
FluorideMode: fluorideMode, FluorideMode: fluorideMode,
DarkMode: darkMode, DarkMode: darkMode,
AntiDopamineMode: antiDopamineMode, AntiDopamineMode: antiDopamineMode,
HideUnsupportedNotifs: hideUnsupportedNotifs,
CSS: css, CSS: css,
} }

View File

@ -298,20 +298,24 @@ function setPos(el, cx, cy, mw, mh) {
} }
var imgPrev = null; var imgPrev = null;
var imgX = 0;
var imgY = 0;
function handleImgPreview(a) { function handleImgPreview(a) {
a.onmouseenter = function(e) { a.onmouseenter = function(e) {
var mw = document.documentElement.clientWidth; var mw = document.documentElement.clientWidth;
var mh = document.documentElement.clientHeight - 24; var mh = document.documentElement.clientHeight - 24;
imgX = e.clientX;
imgY = e.clientY;
var img = document.createElement("img"); var img = document.createElement("img");
img.id = "img-preview"; img.id = "img-preview";
img.src = e.target.getAttribute("href"); img.src = e.target.getAttribute("href");
img.style["max-width"] = mw + "px"; img.style["max-width"] = mw + "px";
img.style["max-height"] = mh + "px"; img.style["max-height"] = mh + "px";
imgPrev = img;
img.onload = function(e2) { img.onload = function(e2) {
setPos(e2.target, e.clientX, e.clientY, mw, mh); setPos(imgPrev, imgX, imgY, mw, mh);
} }
document.body.appendChild(img); document.body.appendChild(img);
imgPrev = img;
} }
a.onmouseleave = function(e) { a.onmouseleave = function(e) {
var img = document.getElementById("img-preview"); var img = document.getElementById("img-preview");
@ -324,7 +328,9 @@ function handleImgPreview(a) {
return; return;
var mw = document.documentElement.clientWidth; var mw = document.documentElement.clientWidth;
var mh = document.documentElement.clientHeight - 24; 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);
} }
} }

View File

@ -517,18 +517,18 @@ img.emoji {
margin-top: 6px; margin-top: 6px;
} }
.notification-title-container { .page-title-container {
margin: 8px 0; margin: 8px 0;
} }
.page-refresh {
margin-right: 8px;
}
.notification-text { .notification-text {
vertical-align: middle; vertical-align: middle;
} }
.notification-refresh {
margin-right: 8px;
}
.notification-read { .notification-read {
display: inline-block; display: inline-block;
} }
@ -575,6 +575,10 @@ kbd {
position: fixed; position: fixed;
} }
.quote {
color: #789922;
}
.dark { .dark {
background-color: #222222; background-color: #222222;
background-image: none; background-image: none;

View File

@ -94,7 +94,7 @@
<td> <kbd>C</kbd> </td> <td> <kbd>C</kbd> </td>
</tr> </tr>
<tr> <tr>
<td> Refresh thread page </td> <td> Refresh timeline/thread page </td>
<td> <kbd>T</kbd> </td> <td> <kbd>T</kbd> </td>
</tr> </tr>
</table> </table>

View File

@ -8,7 +8,7 @@
</div> </div>
<div class="user-info-details-container"> <div class="user-info-details-container">
<div class="user-info-details-name"> <div class="user-info-details-name">
<bdi class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </bdi> <bdi class="status-dname"> {{EmojiFilter (html .User.DisplayName) .User.Emojis}} </bdi>
<a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)"> <a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)">
<span class="status-uname"> @{{.User.Acct}} </span> <span class="status-uname"> @{{.User.Acct}} </span>
</a> </a>

View File

@ -1,13 +1,13 @@
{{with .Data}} {{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}} {{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="notification-title-container"> <div class="page-title-container">
<span class="page-title"> <span class="page-title">
Notifications Notifications
{{if and (not $.Ctx.AntiDopamineMode) (gt .UnreadCount 0)}} {{if and (not $.Ctx.AntiDopamineMode) (gt .UnreadCount 0)}}
({{.UnreadCount }}) ({{.UnreadCount }})
{{end}} {{end}}
</span> </span>
<a class="notification-refresh" href="/notifications" target="_self" accesskey="R" title="Refresh (R)">refresh</a> <a class="page-refresh" href="/notifications" target="_self" accesskey="R" title="Refresh (R)">refresh</a>
{{if .ReadID}} {{if .ReadID}}
<form class="notification-read" action="/notifications/read?max_id={{.ReadID}}" method="post" target="_self"> <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="csrf_token" value="{{$.Ctx.CSRFToken}}">
@ -28,7 +28,7 @@
</div> </div>
<div class="notification-follow"> <div class="notification-follow">
<div class="notification-info-text"> <div class="notification-info-text">
<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi> <bdi class="status-dname"> {{EmojiFilter (html .Account.DisplayName) .Account.Emojis}} </bdi>
<span class="notification-text"> followed you - <span class="notification-text"> followed you -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time> <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span> </span>
@ -48,7 +48,7 @@
</div> </div>
<div class="notification-follow"> <div class="notification-follow">
<div class="notification-info-text"> <div class="notification-info-text">
<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi> <bdi class="status-dname"> {{EmojiFilter (html .Account.DisplayName) .Account.Emojis}} </bdi>
<span class="notification-text"> wants to follow you - <span class="notification-text"> wants to follow you -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time> <time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span> </span>

View File

@ -9,7 +9,7 @@
</div> </div>
<div class="user-list-name"> <div class="user-list-name">
<div> <div>
<div class="status-dname"> {{EmojiFilter .DisplayName .Emojis}} </div> <div class="status-dname"> {{EmojiFilter (html .DisplayName) .Emojis}} </div>
<a class="img-link" href="/user/{{.ID}}"> <a class="img-link" href="/user/{{.ID}}">
<div class="status-uname"> @{{.Acct}} </div> <div class="status-uname"> @{{.Acct}} </div>
</a> </a>

View File

@ -61,6 +61,11 @@
value="true" {{if .Settings.AntiDopamineMode}}checked{{end}}> value="true" {{if .Settings.AntiDopamineMode}}checked{{end}}>
<label for="anti-dopamine-mode"> Enable <abbr title="Remove like/retweet/unread notification count and disable like/retweet/follow notifications">anti-dopamine mode</abbr> </label> <label for="anti-dopamine-mode"> Enable <abbr title="Remove like/retweet/unread notification count and disable like/retweet/follow notifications">anti-dopamine mode</abbr> </label>
</div> </div>
<div class="settings-form-field">
<input id="hide-unsupported-notifs" name="hide_unsupported_notifs" type="checkbox"
value="true" {{if .Settings.HideUnsupportedNotifs}}checked{{end}}>
<label for="hide-unsupported-notifs"> Hide unsupported notifications </label>
</div>
<div class="settings-form-field"> <div class="settings-form-field">
<input id="dark-mode" name="dark_mode" type="checkbox" value="true" {{if .Settings.DarkMode}}checked{{end}}> <input id="dark-mode" name="dark_mode" type="checkbox" value="true" {{if .Settings.DarkMode}}checked{{end}}>
<label for="dark-mode"> Use dark theme </label> <label for="dark-mode"> Use dark theme </label>

View File

@ -5,7 +5,7 @@
<a class="img-link" href="/user/{{.Account.ID}}"> <a class="img-link" href="/user/{{.Account.ID}}">
<img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="24" /> <img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="24" />
</a> </a>
<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi> <bdi class="status-dname"> {{EmojiFilter (html .Account.DisplayName) .Account.Emojis}} </bdi>
<a href="/user/{{.Account.ID}}"> <a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span> <span class="status-uname"> @{{.Account.Acct}} </span>
</a> </a>
@ -23,7 +23,7 @@
</div> </div>
<div class="status"> <div class="status">
<div class="status-name"> <div class="status-name">
<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi> <bdi class="status-dname"> {{EmojiFilter (html .Account.DisplayName) .Account.Emojis}} </bdi>
<a href="/user/{{.Account.ID}}"> <a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span> <span class="status-uname"> @{{.Account.Acct}} </span>
</a> </a>
@ -227,8 +227,8 @@
<div class="status-action status-action-last"> <div class="status-action status-action-last">
<a class="status-time" href="{{if not .ShowReplies}}/thread/{{.ID}}{{end}}#status-{{.ID}}" <a class="status-time" href="{{if not .ShowReplies}}/thread/{{.ID}}{{end}}#status-{{.ID}}"
{{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}> {{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}"> <time datetime="{{FormatTimeRFC3339 .CreatedAt.Time}}" title="{{FormatTimeRFC822 .CreatedAt.Time}}">
{{TimeSince .CreatedAt}} {{TimeSince .CreatedAt.Time}}
</time> </time>
</a> </a>
</div> </div>

View File

@ -1,8 +1,8 @@
{{with $s := .Data}} {{with $s := .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}} {{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="notification-title-container"> <div class="page-title-container">
<span class="page-title"> Thread </span> <span class="page-title"> Thread </span>
<a class="notification-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a> <a class="page-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
</div> </div>
{{range .Statuses}} {{range .Statuses}}

View File

@ -1,6 +1,9 @@
{{with .Data}} {{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}} {{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="page-title"> {{.Title}} </div> <div class="page-title-container">
<span class="page-title"> {{.Title}} </span>
<a class="page-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
</div>
{{if eq .Type "remote"}} {{if eq .Type "remote"}}
<form class="search-form" action="/timeline/remote" method="GET"> <form class="search-form" action="/timeline/remote" method="GET">

View File

@ -11,7 +11,7 @@
</div> </div>
<div class="user-profile-details-container"> <div class="user-profile-details-container">
<div> <div>
<bdi class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </bdi> <bdi class="status-dname"> {{EmojiFilter (html .User.DisplayName) .User.Emojis}} </bdi>
<span class="status-uname"> @{{.User.Acct}} </span> <span class="status-uname"> @{{.User.Acct}} </span>
<a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile"> <a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile">
source source
@ -83,6 +83,12 @@
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}"> <input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="submit" value="mute" class="btn-link"> <input type="submit" value="mute" class="btn-link">
</form> </form>
-
<form class="d-inline" action="/mute/{{.User.ID}}?notifications=false" method="post">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="submit" value="mute (keep notifications)" class="btn-link">
</form>
{{end}} {{end}}
{{if .User.Pleroma.Relationship.Following}} {{if .User.Pleroma.Relationship.Following}}
- -

View File

@ -8,7 +8,7 @@
</a> </a>
</div> </div>
<div class="user-list-name"> <div class="user-list-name">
<div class="status-dname"> {{EmojiFilter .DisplayName .Emojis}} </div> <div class="status-dname"> {{EmojiFilter (html .DisplayName) .Emojis}} </div>
<a class="img-link" href="/user/{{.ID}}"> <a class="img-link" href="/user/{{.ID}}">
<div class="status-uname"> @{{.Acct}} </div> <div class="status-uname"> @{{.Acct}} </div>
</a> </a>

View File

@ -1,6 +1,6 @@
{{with .Data}} {{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}} {{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="page-title"> Search {{EmojiFilter .User.DisplayName .User.Emojis}}'s statuses </div> <div class="page-title"> Search {{EmojiFilter (html .User.DisplayName) .User.Emojis}}'s statuses </div>
<form class="search-form" action="/usersearch/{{.User.ID}}" method="GET"> <form class="search-form" action="/usersearch/{{.User.ID}}" method="GET">
<span class="post-form-field> <span class="post-form-field>

View File

@ -1,122 +0,0 @@
/*
Copyright 2019 Drew DeVault <sir@cmpwn.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package util
import (
"fmt"
"os"
)
// In the case of "-o example", Option is 'o' and "example" is Value. For
// options which do not take an argument, Value is "".
type Option struct {
Option rune
Value string
}
// This is returned when an unknown option is found in argv, but not in the
// option spec.
type UnknownOptionError rune
func (e UnknownOptionError) Error() string {
return fmt.Sprintf("%s: unknown option -%c", os.Args[0], rune(e))
}
// This is returned when an option with a mandatory argument is missing that
// argument.
type MissingOptionError rune
func (e MissingOptionError) Error() string {
return fmt.Sprintf("%s: expected argument for -%c", os.Args[0], rune(e))
}
// Getopts implements a POSIX-compatible options interface.
//
// Returns a slice of options and the index of the first non-option argument.
//
// If an error is returned, you must print it to stderr to be POSIX complaint.
func Getopts(argv []string, spec string) ([]Option, int, error) {
optmap := make(map[rune]bool)
runes := []rune(spec)
for i, rn := range spec {
if rn == ':' {
if i == 0 {
continue
}
optmap[runes[i-1]] = true
} else {
optmap[rn] = false
}
}
var (
i int
opts []Option
)
for i = 1; i < len(argv); i++ {
arg := argv[i]
runes = []rune(arg)
if len(arg) == 0 || arg == "-" {
break
}
if arg[0] != '-' {
break
}
if arg == "--" {
i++
break
}
for j, opt := range runes[1:] {
if optopt, ok := optmap[opt]; !ok {
opts = append(opts, Option{'?', ""})
return opts, i, UnknownOptionError(opt)
} else if optopt {
if j+1 < len(runes)-1 {
opts = append(opts, Option{opt, string(runes[j+2:])})
break
} else {
if i+1 >= len(argv) {
if len(spec) >= 1 && spec[0] == ':' {
opts = append(opts, Option{':', string(opt)})
} else {
return opts, i, MissingOptionError(opt)
}
} else {
opts = append(opts, Option{opt, argv[i+1]})
i++
}
}
} else {
opts = append(opts, Option{opt, ""})
}
}
}
return opts, i, nil
}

View File

@ -2,24 +2,18 @@ package util
import ( import (
"crypto/rand" "crypto/rand"
"math/big" "encoding/base64"
) )
var ( var enc = base64.URLEncoding
runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
runes_length = len(runes)
)
func NewRandID(n int) (string, error) { func NewRandID(n int) (string, error) {
data := make([]rune, n) data := make([]byte, enc.DecodedLen(n))
for i := range data { _, err := rand.Read(data)
num, err := rand.Int(rand.Reader, big.NewInt(int64(runes_length)))
if err != nil { if err != nil {
return "", err return "", err
} }
data[i] = runes[num.Int64()] return enc.EncodeToString(data), nil
}
return string(data), nil
} }
func NewSessionID() (string, error) { func NewSessionID() (string, error) {