diff --git a/bloat.conf b/bloat.conf index e583988..5c6a02b 100644 --- a/bloat.conf +++ b/bloat.conf @@ -31,6 +31,9 @@ static_directory=static # Empty value will disable the format selection in frontend. post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:text/bbcode +# Log file. Will log to stdout if value is empty. +# log_file=log + # In single instance mode, bloat will not ask for instance domain name and # user will be directly redirected to login form. User login from other # instances is not allowed in this mode. diff --git a/config/config.go b/config/config.go index 141cb39..a92d66f 100644 --- a/config/config.go +++ b/config/config.go @@ -20,6 +20,7 @@ type config struct { TemplatesPath string CustomCSS string PostFormats []model.PostFormat + LogFile string } func (c *config) IsValid() bool { @@ -96,7 +97,7 @@ func Parse(r io.Reader) (c *config, err error) { } c.PostFormats = formats case "log_file": - // ignore + c.LogFile = val default: return nil, errors.New("invalid config key " + key) } diff --git a/go.mod b/go.mod index ac3bac6..8eb42a7 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,4 @@ require ( github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 ) -go 1.11 +go 1.13 diff --git a/main.go b/main.go index 657912d..b717b7d 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,6 @@ func errExit(err error) { func main() { configFile := flag.String("f", "", "config file") - verbose := flag.Bool("v", false, "verbose mode") flag.Parse() if len(*configFile) > 0 { @@ -53,12 +52,25 @@ func main() { customCSS = "/static/" + customCSS } + var logger *log.Logger + if len(config.LogFile) < 1 { + logger = log.New(os.Stdout, "", log.LstdFlags) + } else { + lf, err := os.OpenFile(config.LogFile, + os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + errExit(err) + } + defer lf.Close() + logger = log.New(lf, "", log.LstdFlags) + } + s := service.NewService(config.ClientName, config.ClientScope, config.ClientWebsite, customCSS, config.SingleInstance, config.PostFormats, renderer) - handler := service.NewHandler(s, *verbose, config.StaticDirectory) + handler := service.NewHandler(s, logger, config.StaticDirectory) - log.Println("listening on", config.ListenAddress) + logger.Println("listening on", config.ListenAddress) err = http.ListenAndServe(config.ListenAddress, handler) if err != nil { errExit(err) diff --git a/mastodon/accounts.go b/mastodon/accounts.go index 9057cb3..b98735f 100644 --- a/mastodon/accounts.go +++ b/mastodon/accounts.go @@ -1,15 +1,11 @@ package mastodon import ( - "bytes" "context" "fmt" "encoding/json" - "io" - "mime/multipart" "net/http" "net/url" - "path/filepath" "strconv" "time" "strings" @@ -46,7 +42,6 @@ type Account struct { Moved *Account `json:"moved"` Fields []Field `json:"fields"` Bot bool `json:"bot"` - Source *AccountSource `json:"source"` Pleroma *AccountPleroma `json:"pleroma"` MastodonAccount bool } @@ -185,12 +180,13 @@ type Profile struct { Source *AccountSource // Set the base64 encoded character string of the image. - Avatar *multipart.FileHeader - Header *multipart.FileHeader + Avatar string + Header string //Other settings Bot *bool Pleroma *ProfilePleroma + } type ProfilePleroma struct { @@ -203,116 +199,61 @@ type ProfilePleroma struct { HideFollows *bool HideFollowersCount *bool HideFollowsCount *bool - Avatar *multipart.FileHeader - Header *multipart.FileHeader } // AccountUpdate updates the information of the current user. func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, error) { - var buf bytes.Buffer - mw := multipart.NewWriter(&buf) + params := url.Values{} if profile.DisplayName != nil { - err := mw.WriteField("display_name", *profile.DisplayName) - if err != nil { - return nil, err - } + params.Set("display_name", *profile.DisplayName) } if profile.Note != nil { - err := mw.WriteField("note", *profile.Note) - if err != nil { - return nil, err - } + params.Set("note", *profile.Note) } if profile.Locked != nil { - err := mw.WriteField("locked", strconv.FormatBool(*profile.Locked)) - if err != nil { - return nil, err - } + params.Set("locked", strconv.FormatBool(*profile.Locked)) } if profile.Fields != nil { for idx, field := range *profile.Fields { - err := mw.WriteField(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name) - if err != nil { - return nil, err - } - err = mw.WriteField(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value) - if err != nil { - return nil, err - } + params.Set(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name) + params.Set(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value) } } - if profile.Avatar != nil { - f, err := profile.Avatar.Open() - if err != nil { - return nil, err + if profile.Source != nil { + if profile.Source.Privacy != nil { + params.Set("source[privacy]", *profile.Source.Privacy) } - fname := filepath.Base(profile.Avatar.Filename) - part, err := mw.CreateFormFile("avatar", fname) - if err != nil { - return nil, err + if profile.Source.Sensitive != nil { + params.Set("source[sensitive]", strconv.FormatBool(*profile.Source.Sensitive)) } - _, err = io.Copy(part, f) - if err != nil { - return nil, err + if profile.Source.Language != nil { + params.Set("source[language]", *profile.Source.Language) } } - if profile.Header != nil { - f, err := profile.Header.Open() - if err != nil { - return nil, err - } - fname := filepath.Base(profile.Header.Filename) - part, err := mw.CreateFormFile("header", fname) - if err != nil { - return nil, err - } - _, err = io.Copy(part, f) - if err != nil { - return nil, err - } + if profile.Avatar != "" { + params.Set("avatar", profile.Avatar) } - err := mw.Close() - if err != nil { - return nil, err + if profile.Header != "" { + params.Set("header", profile.Header) + } + + if profile.Bot != nil { + params.Set("bot", strconv.FormatBool(*profile.Bot)) + } + if profile.Pleroma != nil { + if profile.Pleroma.AcceptsChatMessages != nil { + params.Set("accepts_chat_messages", strconv.FormatBool(*profile.Pleroma.AcceptsChatMessages)) + } } - params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()} var account Account - err = c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil) + err := c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil) if err != nil { return nil, err } return &account, nil } -func (c *Client) accountDeleteField(ctx context.Context, field string) (*Account, error) { - var buf bytes.Buffer - mw := multipart.NewWriter(&buf) - _, err := mw.CreateFormField(field) - if err != nil { - return nil, err - } - err = mw.Close() - if err != nil { - return nil, err - } - params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()} - var account Account - err = c.doAPI(ctx, http.MethodPatch, "/api/v1/accounts/update_credentials", params, &account, nil) - if err != nil { - return nil, err - } - return &account, nil -} - -func (c *Client) AccountDeleteAvatar(ctx context.Context) (*Account, error) { - return c.accountDeleteField(ctx, "avatar") -} - -func (c *Client) AccountDeleteHeader(ctx context.Context) (*Account, error) { - return c.accountDeleteField(ctx, "header") -} - // GetAccountStatuses return statuses by specified accuont. func (c *Client) GetAccountStatuses(ctx context.Context, id string, onlyMedia bool, onlyPinned bool, pg *Pagination) ([]*Status, error) { var statuses []*Status diff --git a/mastodon/apps.go b/mastodon/apps.go index 1ce894f..ca24e0b 100644 --- a/mastodon/apps.go +++ b/mastodon/apps.go @@ -11,6 +11,7 @@ import ( // AppConfig is a setting for registering applications. type AppConfig struct { + http.Client Server string ClientName string @@ -60,7 +61,7 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error } req = req.WithContext(ctx) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - resp, err := httpClient.Do(req) + resp, err := appConfig.Do(req) if err != nil { return nil, err } diff --git a/mastodon/http.go b/mastodon/http.go deleted file mode 100644 index 7d1c1c4..0000000 --- a/mastodon/http.go +++ /dev/null @@ -1,45 +0,0 @@ -package mastodon - -import ( - "fmt" - "io" - "net/http" - "time" -) - -type lr struct { - io.ReadCloser - n int64 - r *http.Request -} - -func (r *lr) Read(p []byte) (n int, err error) { - if r.n <= 0 { - return 0, fmt.Errorf("%s \"%s\": response body too large", r.r.Method, r.r.URL) - } - if int64(len(p)) > r.n { - p = p[0:r.n] - } - n, err = r.ReadCloser.Read(p) - r.n -= int64(n) - return -} - -type transport struct { - t http.RoundTripper -} - -func (t *transport) RoundTrip(r *http.Request) (*http.Response, error) { - resp, err := t.t.RoundTrip(r) - if resp != nil && resp.Body != nil { - resp.Body = &lr{resp.Body, 8 << 20, r} - } - return resp, err -} - -var httpClient = &http.Client{ - Transport: &transport{ - t: http.DefaultTransport, - }, - Timeout: 30 * time.Second, -} diff --git a/mastodon/mastodon.go b/mastodon/mastodon.go index 7aafdcf..3c2a6bb 100644 --- a/mastodon/mastodon.go +++ b/mastodon/mastodon.go @@ -2,15 +2,19 @@ package mastodon import ( + "bytes" "context" "encoding/json" "compress/gzip" "errors" "fmt" "io" + "mime/multipart" "net/http" "net/url" + "os" "path" + "path/filepath" "strings" "github.com/tomnomnom/linkheader" @@ -30,11 +34,6 @@ type Client struct { config *Config } -type multipartRequest struct { - Data io.Reader - ContentType string -} - func (c *Client) doAPI(ctx context.Context, method string, uri string, params interface{}, res interface{}, pg *Pagination) error { u, err := url.Parse(c.config.Server) if err != nil { @@ -58,12 +57,83 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in if err != nil { return err } - } else if mr, ok := params.(*multipartRequest); ok { - req, err = http.NewRequest(method, u.String(), mr.Data) + } else if file, ok := params.(string); ok { + f, err := os.Open(file) if err != nil { return err } - ct = mr.ContentType + defer f.Close() + + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + part, err := mw.CreateFormFile("file", filepath.Base(file)) + if err != nil { + return err + } + _, err = io.Copy(part, f) + if err != nil { + return err + } + err = mw.Close() + if err != nil { + return err + } + req, err = http.NewRequest(method, u.String(), &buf) + if err != nil { + return err + } + ct = mw.FormDataContentType() + } else if file, ok := params.(*multipart.FileHeader); ok { + f, err := file.Open() + if err != nil { + return err + } + defer f.Close() + + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + 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 + } + _, err = io.Copy(part, f) + if err != nil { + return err + } + err = mw.Close() + if err != nil { + return err + } + req, err = http.NewRequest(method, u.String(), &buf) + if err != nil { + return err + } + ct = mw.FormDataContentType() + } else if reader, ok := params.(io.Reader); ok { + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + part, err := mw.CreateFormFile("file", "upload") + if err != nil { + return err + } + _, err = io.Copy(part, reader) + if err != nil { + return err + } + err = mw.Close() + if err != nil { + return err + } + req, err = http.NewRequest(method, u.String(), &buf) + if err != nil { + return err + } + ct = mw.FormDataContentType() } else { if method == http.MethodGet && pg != nil { u.RawQuery = pg.toValues().Encode() @@ -113,7 +183,7 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in // NewClient return new mastodon API client. func NewClient(config *Config) *Client { return &Client{ - Client: httpClient, + Client: http.DefaultClient, config: config, } } @@ -149,16 +219,6 @@ func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI st return c.authenticate(ctx, params) } -func (c *Client) RevokeToken(ctx context.Context) error { - params := url.Values{ - "client_id": {c.config.ClientID}, - "client_secret": {c.config.ClientSecret}, - "token": {c.GetAccessToken(ctx)}, - } - - return c.doAPI(ctx, http.MethodPost, "/oauth/revoke", params, nil, nil) -} - func (c *Client) authenticate(ctx context.Context, params url.Values) error { u, err := url.Parse(c.config.Server) if err != nil { diff --git a/mastodon/status.go b/mastodon/status.go index 54961ec..e148720 100644 --- a/mastodon/status.go +++ b/mastodon/status.go @@ -1,18 +1,13 @@ package mastodon import ( - "bytes" "context" "fmt" "io" "mime/multipart" "net/http" "net/url" - "path/filepath" "time" - "encoding/json" - "path" - "strings" ) type StatusPleroma struct { @@ -229,51 +224,6 @@ func (c *Client) GetTimelineHome(ctx context.Context, pg *Pagination) ([]*Status return statuses, nil } -type RemoteTimelineInstance struct { - http.Client -} - -// TrueRemoteTimeline get public timeline from remote Mastodon API compatible instance directly -func (c *Client) TrueRemoteTimeline(ctx context.Context, instance string, pg *Pagination) ([]*Status, error) { - var httpclient RemoteTimelineInstance - var publicstatuses []*Status - params := url.Values{} - params.Set("local", "true") - if pg != nil { - params = pg.setValues(params) - } - u, err := url.Parse("https://" + instance) - if err != nil { - return nil, err - } - u.Path = path.Join(u.Path, "/api/v1/timelines/public") - - req, err := http.NewRequest(http.MethodGet, u.String(), strings.NewReader(params.Encode())) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - resp, err := httpclient.Do(req) - fmt.Println(req) - fmt.Println(resp) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, parseAPIError("bad request", resp) - } - - err = json.NewDecoder(resp.Body).Decode(&publicstatuses) - fmt.Println(resp.Body) - if err != nil { - return nil, err - } - return publicstatuses, nil -} - // GetTimelinePublic return statuses from public timeline. func (c *Client) GetTimelinePublic(ctx context.Context, isLocal bool, instance string, pg *Pagination) ([]*Status, error) { params := url.Values{} @@ -407,35 +357,30 @@ func (c *Client) Search(ctx context.Context, q string, qType string, limit int, return &results, nil } -func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) { - f, err := fh.Open() - if err != nil { - return nil, err - } - defer f.Close() - - var buf bytes.Buffer - mw := multipart.NewWriter(&buf) - fname := filepath.Base(fh.Filename) - err = mw.WriteField("description", fname) - if err != nil { - return nil, err - } - part, err := mw.CreateFormFile("file", fname) - if err != nil { - return nil, err - } - _, err = io.Copy(part, f) - if err != nil { - return nil, err - } - err = mw.Close() - if err != nil { - return nil, err - } - params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()} +// UploadMedia upload a media attachment from a file. +func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) { var attachment Attachment - err = c.doAPI(ctx, http.MethodPost, "/api/v1/media", params, &attachment, nil) + err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil) + if err != nil { + return nil, err + } + return &attachment, nil +} + +// UploadMediaFromReader uploads a media attachment from a io.Reader. +func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*Attachment, error) { + var attachment Attachment + err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", reader, &attachment, nil) + if err != nil { + return nil, err + } + return &attachment, nil +} + +// UploadMediaFromReader uploads a media attachment from a io.Reader. +func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) { + var attachment Attachment + err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", fh, &attachment, nil) if err != nil { return nil, err } diff --git a/renderer/model.go b/renderer/model.go index 1d3d414..c636571 100644 --- a/renderer/model.go +++ b/renderer/model.go @@ -177,11 +177,6 @@ type FiltersData struct { Filters []*mastodon.Filter } -type ProfileData struct { - *CommonData - User *mastodon.Account -} - type MuteData struct { *CommonData User *mastodon.Account diff --git a/renderer/renderer.go b/renderer/renderer.go index 599be56..3e95a7d 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -36,7 +36,6 @@ const ( SettingsPage = "settings.tmpl" UserEditPage = "useredit.tmpl" FiltersPage = "filters.tmpl" - ProfilePage = "profile.tmpl" MutePage = "mute.tmpl" ) diff --git a/service/client.go b/service/client.go index 7c5c33c..7cebbf1 100644 --- a/service/client.go +++ b/service/client.go @@ -4,7 +4,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "net/http" "strings" "time" @@ -69,7 +68,7 @@ func (c *client) redirect(url string) { c.w.WriteHeader(http.StatusFound) } -func (c *client) authenticate(t int, instance string) (err error) { +func (c *client) authenticate(t int) (err error) { csrf := c.r.FormValue("csrf_token") ref := c.r.URL.RequestURI() defer func() { @@ -101,9 +100,6 @@ func (c *client) authenticate(t int, instance string) (err error) { return err } c.s = sess - if len(instance) > 0 && c.s.Instance != instance { - return errors.New("invalid instance") - } c.Client = mastodon.NewClient(&mastodon.Config{ Server: "https://" + c.s.Instance, ClientID: c.s.ClientID, diff --git a/service/service.go b/service/service.go index 6870f09..4643dae 100644 --- a/service/service.go +++ b/service/service.go @@ -149,20 +149,12 @@ func (s *service) TimelinePage(c *client, tType, instance, listId, maxID, title = "Local Timeline" case "remote": if len(instance) > 0 { - statuses, err = c.GetTimelinePublic(c.ctx, true, instance, &pg) + statuses, err = c.GetTimelinePublic(c.ctx, false, instance, &pg) if err != nil { return err } } title = "Remote Timeline" - case "tremote": - if len(instance) > 0 { - statuses, err = c.TrueRemoteTimeline(c.ctx, instance, &pg) - if err != nil { - return err - } - } - title = "True Remote Timeline" case "twkn": statuses, err = c.GetTimelinePublic(c.ctx, false, "", &pg) if err != nil { @@ -747,7 +739,7 @@ func (s *service) MutePage(c *client, id string) (err error) { if err != nil { return } - cdata := s.cdata(c, "Mute "+user.DisplayName+" @"+user.Acct, 0, 0, "") + cdata := s.cdata(c, "Mute"+user.DisplayName+" @"+user.Acct, 0, 0, "") data := &renderer.UserData{ User: user, CommonData: cdata, @@ -870,55 +862,6 @@ func (svc *service) FiltersPage(c *client) (err error) { return svc.renderer.Render(c.rctx, c.w, renderer.FiltersPage, data) } -func (svc *service) ProfilePage(c *client) (err error) { - u, err := c.GetAccountCurrentUser(c.ctx) - if err != nil { - return - } - // Some instances allow more than 4 fields, but make sure that there are - // at least 4 fields in the slice because the template depends on it. - if u.Source.Fields == nil { - u.Source.Fields = new([]mastodon.Field) - } - for len(*u.Source.Fields) < 4 { - *u.Source.Fields = append(*u.Source.Fields, mastodon.Field{}) - } - cdata := svc.cdata(c, "edit profile", 0, 0, "") - data := &renderer.ProfileData{ - CommonData: cdata, - User: u, - } - return svc.renderer.Render(c.rctx, c.w, renderer.ProfilePage, data) -} - -func (s *service) ProfileUpdate(c *client, name, bio string, avatar, banner *multipart.FileHeader, - fields []mastodon.Field, locked bool) (err error) { - // Need to pass empty data to clear fields - if len(fields) == 0 { - fields = append(fields, mastodon.Field{}) - } - p := &mastodon.Profile{ - DisplayName: &name, - Note: &bio, - Avatar: avatar, - Header: banner, - Fields: &fields, - Locked: &locked, - } - _, err = c.AccountUpdate(c.ctx, p) - return err -} - -func (s *service) ProfileDelAvatar(c *client) (err error) { - _, err = c.AccountDeleteAvatar(c.ctx) - return -} - -func (s *service) ProfileDelBanner(c *client) (err error) { - _, err = c.AccountDeleteHeader(c.ctx) - return err -} - func (s *service) SingleInstance() (instance string, ok bool) { if len(s.instance) > 0 { instance = s.instance @@ -1063,9 +1006,6 @@ func (s *service) NewSessionRegister(c *client, instance string, reason string, return } -func (s *service) Signout(c *client) (err error) { - return c.RevokeToken(c.ctx) -} func (s *service) Post(c *client, content string, replyToID string, format string, visibility string, isNSFW bool, spoilerText string, diff --git a/service/transport.go b/service/transport.go index c5bdde7..ccec389 100644 --- a/service/transport.go +++ b/service/transport.go @@ -2,9 +2,7 @@ package service import ( "encoding/json" - "fmt" "log" - "mime/multipart" "net/http" "strconv" "time" @@ -26,7 +24,7 @@ const ( CSRF ) -func NewHandler(s *service, verbose bool, staticDir string) http.Handler { +func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler { r := mux.NewRouter() writeError := func(c *client, err error, t int, retry bool) { @@ -51,12 +49,10 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { r: req, } - if verbose { - defer func(begin time.Time) { - log.Printf("path=%s, err=%v, took=%v\n", - req.URL.Path, err, time.Since(begin)) - }(time.Now()) - } + defer func(begin time.Time) { + logger.Printf("path=%s, err=%v, took=%v\n", + req.URL.Path, err, time.Since(begin)) + }(time.Now()) var ct string switch rt { @@ -67,7 +63,7 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { } c.w.Header().Add("Content-Type", ct) - err = c.authenticate(at, s.instance) + err = c.authenticate(at) if err != nil { writeError(c, err, rt, req.Method == http.MethodGet) return @@ -82,7 +78,7 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { } rootPage := handle(func(c *client) error { - err := c.authenticate(SESSION, "") + err := c.authenticate(SESSION) if err != nil { if err == errInvalidSession { c.redirect("/signin") @@ -207,6 +203,43 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { return s.SearchPage(c, sq, qType, offset) }, SESSION, HTML) + userEditPage := handle(func(c *client) error { + return s.UserEditPage(c) + }, SESSION, HTML) + + userEdit := handle(func(c *client) error { + displayName := c.r.FormValue("display-name") + note := c.r.FormValue("note") + locked := c.r.FormValue("locked") == "true" + bot := c.r.FormValue("bot") == "true" + acceptsChatMessages := c.r.FormValue("accepts-chat-messages") == "true" + hideFavourites := c.r.FormValue("hide-favourites") == "true" + + pleromaProfile := mastodon.ProfilePleroma{ + AcceptsChatMessages: &acceptsChatMessages, + HideFavourites: &hideFavourites, + } + + usersettings := mastodon.Profile{ + DisplayName: &displayName, + Note: ¬e, + Locked: &locked, + Fields: nil, + Source: nil, + Avatar: "", + Header: "", + Bot: &bot, + Pleroma: &pleromaProfile, + } + + err := s.UserSave(c, usersettings) + if err != nil { + return err + } + c.redirect("/user/"+c.r.FormValue("id")) + return nil + }, SESSION, HTML) + settingsPage := handle(func(c *client) error { return s.SettingsPage(c) }, SESSION, HTML) @@ -215,57 +248,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { return s.FiltersPage(c) }, SESSION, HTML) - profilePage := handle(func(c *client) error { - return s.ProfilePage(c) - }, SESSION, HTML) - - profileUpdate := handle(func(c *client) error { - name := c.r.FormValue("name") - bio := c.r.FormValue("bio") - var avatar, banner *multipart.FileHeader - if f := c.r.MultipartForm.File["avatar"]; len(f) > 0 { - avatar = f[0] - } - if f := c.r.MultipartForm.File["banner"]; len(f) > 0 { - banner = f[0] - } - var fields []mastodon.Field - for i := 0; i < 16; i++ { - n := c.r.FormValue(fmt.Sprintf("field-name-%d", i)) - v := c.r.FormValue(fmt.Sprintf("field-value-%d", i)) - if len(n) == 0 { - continue - } - f := mastodon.Field{Name: n, Value: v} - fields = append(fields, f) - } - locked := c.r.FormValue("locked") == "true" - err := s.ProfileUpdate(c, name, bio, avatar, banner, fields, locked) - if err != nil { - return err - } - c.redirect("/") - return nil - }, CSRF, HTML) - - profileDelAvatar := handle(func(c *client) error { - err := s.ProfileDelAvatar(c) - if err != nil { - return err - } - c.redirect(c.r.FormValue("referrer")) - return nil - }, CSRF, HTML) - - profileDelBanner := handle(func(c *client) error { - err := s.ProfileDelBanner(c) - if err != nil { - return err - } - c.redirect(c.r.FormValue("referrer")) - return nil - }, CSRF, HTML) - signin := handle(func(c *client) error { instance := c.r.FormValue("instance") url, sess, err := s.NewSession(c, instance) @@ -768,10 +750,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { }, CSRF, HTML) signout := handle(func(c *client) error { - err := s.Signout(c) - if err != nil { - return err - } c.unsetSession() c.redirect("/") return nil @@ -833,12 +811,10 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler { r.HandleFunc("/aboutinstance", aboutInstance).Methods(http.MethodGet) r.HandleFunc("/emojis", emojisPage).Methods(http.MethodGet) r.HandleFunc("/search", searchPage).Methods(http.MethodGet) + r.HandleFunc("/useredit", userEditPage).Methods(http.MethodGet) + r.HandleFunc("/useredit", userEdit).Methods(http.MethodPost) r.HandleFunc("/settings", settingsPage).Methods(http.MethodGet) r.HandleFunc("/filters", filtersPage).Methods(http.MethodGet) - r.HandleFunc("/profile", profilePage).Methods(http.MethodGet) - r.HandleFunc("/profile", profileUpdate).Methods(http.MethodPost) - r.HandleFunc("/profile/delavatar", profileDelAvatar).Methods(http.MethodPost) - r.HandleFunc("/profile/delbanner", profileDelBanner).Methods(http.MethodPost) r.HandleFunc("/signin", signin).Methods(http.MethodPost) r.HandleFunc("/signup", signup).Methods(http.MethodPost) r.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet) diff --git a/static/fluoride.js b/static/fluoride.js index ee3e3d8..132008a 100644 --- a/static/fluoride.js +++ b/static/fluoride.js @@ -286,12 +286,6 @@ function onPaste(e) { fp.files = dt.files; } -function onKeydown(e) { - if (e.key == 'Enter' && e.ctrlKey) { - document.querySelector(".post-form").submit(); - } -} - document.addEventListener("DOMContentLoaded", function() { checkCSRFToken(); checkAntiDopamineMode(); @@ -332,10 +326,8 @@ document.addEventListener("DOMContentLoaded", function() { } var pf = document.querySelector(".post-form") - if (pf) { + if (pf) pf.addEventListener("paste", onPaste); - pf.addEventListener("keydown", onKeydown); - } }); // @license-end diff --git a/static/style.css b/static/style.css index 175d1df..86cb61c 100644 --- a/static/style.css +++ b/static/style.css @@ -1,4 +1,4 @@ -frame, body { +body { background-color: #d2d2d2; } @@ -167,14 +167,15 @@ textarea { padding: 4px; font-size: 11pt; font-family: initial; - box-sizing: border-box; } .post-content { + box-sizing: border-box; width: 100%; } -#css, #bio { +#css { + box-sizing: border-box; max-width: 100%; } @@ -441,14 +442,9 @@ img.emoji { margin-right: 2px; } -.profile-edit-link { - font-size: 8pt; -} - .user-list-item { overflow: auto; - margin: 0 0 4px 0; - padding: 4px; + margin: 0 0 12px 0; display: flex; align-items: center; } @@ -600,41 +596,6 @@ kbd { color: #789922; } -.profile-form { - margin: 0 4px; -} - -.profile-form-field { - margin: 8px 0; -} - -.profile-avatar { - height: 96px; - width: 96px; - object-fit: contain; -} - -.profile-banner { - height: 120px; -} - -.block-label, -.profile-delete, -.profile-field, -.profile-field input { - margin: 0 0 4px 0; -} - -.profile-form input[type=text] { - width: 320px; - max-width: 100%; - box-sizing: border-box; -} - -#bio { - width: 644px; -} - .dark { background-color: #222222; background-image: none; @@ -645,7 +606,7 @@ kbd { color: #81a2be; } -.dark .post-content { +.dark textarea { background-color: #333333; border: 1px solid #444444; color: #eaeaea; diff --git a/templates/mute.tmpl b/templates/mute.tmpl index ee66b91..47d0533 100644 --- a/templates/mute.tmpl +++ b/templates/mute.tmpl @@ -1,6 +1,6 @@ {{with .Data}} {{template "header.tmpl" (WithContext .CommonData $.Ctx)}} -