mirror of
https://gitea.phreedom.club/localhost_frssoft/bloat.git
synced 2024-11-24 21:59:21 +02:00
Merge branch 'master' into absolute_fluoride
This commit is contained in:
commit
b8c0133bcd
|
@ -1,3 +1,2 @@
|
||||||
bloat
|
bloat
|
||||||
database
|
database
|
||||||
bloat.def.conf
|
|
||||||
|
|
12
INSTALL
12
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.
|
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
|
||||||
|
|
13
Makefile
13
Makefile
|
@ -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
4
README
|
@ -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.
|
||||||
|
|
|
@ -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) {
|
||||||
f, err := os.Open(file)
|
var lastErr error
|
||||||
if err != nil {
|
for _, file := range files {
|
||||||
return
|
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()
|
if lastErr == nil {
|
||||||
|
lastErr = errors.New("invalid config file")
|
||||||
info, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return nil, lastErr
|
||||||
if info.IsDir() {
|
|
||||||
return nil, errors.New("invalid config file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return Parse(f)
|
|
||||||
}
|
}
|
||||||
|
|
19
main.go
19
main.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -1,31 +1,33 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
DefaultVisibility string `json:"default_visibility"`
|
DefaultVisibility string `json:"default_visibility"`
|
||||||
DefaultFormat string `json:"default_format"`
|
DefaultFormat string `json:"default_format"`
|
||||||
CopyScope bool `json:"copy_scope"`
|
CopyScope bool `json:"copy_scope"`
|
||||||
ThreadInNewTab bool `json:"thread_in_new_tab"`
|
ThreadInNewTab bool `json:"thread_in_new_tab"`
|
||||||
HideAttachments bool `json:"hide_attachments"`
|
HideAttachments bool `json:"hide_attachments"`
|
||||||
MaskNSFW bool `json:"mask_nfsw"`
|
MaskNSFW bool `json:"mask_nfsw"`
|
||||||
NotificationInterval int `json:"notifications_interval"`
|
NotificationInterval int `json:"notifications_interval"`
|
||||||
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"`
|
||||||
CSS string `json:"css"`
|
HideUnsupportedNotifs bool `json:"hide_unsupported_notifs"`
|
||||||
|
CSS string `json:"css"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSettings() *Settings {
|
func NewSettings() *Settings {
|
||||||
return &Settings{
|
return &Settings{
|
||||||
DefaultVisibility: "public",
|
DefaultVisibility: "public",
|
||||||
DefaultFormat: "",
|
DefaultFormat: "",
|
||||||
CopyScope: true,
|
CopyScope: true,
|
||||||
ThreadInNewTab: false,
|
ThreadInNewTab: false,
|
||||||
HideAttachments: false,
|
HideAttachments: false,
|
||||||
MaskNSFW: true,
|
MaskNSFW: true,
|
||||||
NotificationInterval: 0,
|
NotificationInterval: 0,
|
||||||
FluorideMode: false,
|
FluorideMode: false,
|
||||||
DarkMode: false,
|
DarkMode: false,
|
||||||
AntiDopamineMode: false,
|
AntiDopamineMode: false,
|
||||||
CSS: "",
|
HideUnsupportedNotifs: false,
|
||||||
|
CSS: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)(>.*)(<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+`"`)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,20 +490,22 @@ 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{
|
||||||
DefaultVisibility: visibility,
|
DefaultVisibility: visibility,
|
||||||
DefaultFormat: format,
|
DefaultFormat: format,
|
||||||
CopyScope: copyScope,
|
CopyScope: copyScope,
|
||||||
ThreadInNewTab: threadInNewTab,
|
ThreadInNewTab: threadInNewTab,
|
||||||
HideAttachments: hideAttachments,
|
HideAttachments: hideAttachments,
|
||||||
MaskNSFW: maskNSFW,
|
MaskNSFW: maskNSFW,
|
||||||
NotificationInterval: ni,
|
NotificationInterval: ni,
|
||||||
FluorideMode: fluorideMode,
|
FluorideMode: fluorideMode,
|
||||||
DarkMode: darkMode,
|
DarkMode: darkMode,
|
||||||
AntiDopamineMode: antiDopamineMode,
|
AntiDopamineMode: antiDopamineMode,
|
||||||
CSS: css,
|
HideUnsupportedNotifs: hideUnsupportedNotifs,
|
||||||
|
CSS: css,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.SaveSettings(c, settings)
|
err := s.SaveSettings(c, settings)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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}}
|
||||||
-
|
-
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
122
util/getopt.go
122
util/getopt.go
|
@ -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
|
|
||||||
}
|
|
20
util/rand.go
20
util/rand.go
|
@ -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 string(data), nil
|
return enc.EncodeToString(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSessionID() (string, error) {
|
func NewSessionID() (string, error) {
|
||||||
|
|
Loading…
Reference in New Issue