mirror of
https://git.phreedom.club/localhost_frssoft/bloat.git
synced 2024-11-29 07:01:28 +00:00
Compare commits
No commits in common. "96a0d7cf05c84f8799d5946b3ded0e81623d7ddf" and "fa5eaa4442a8d9b088c5816b826c98be2b04c66b" have entirely different histories.
96a0d7cf05
...
fa5eaa4442
|
@ -31,6 +31,9 @@ static_directory=static
|
||||||
# Empty value will disable the format selection in frontend.
|
# Empty value will disable the format selection in frontend.
|
||||||
post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:text/bbcode
|
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
|
# 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
|
# user will be directly redirected to login form. User login from other
|
||||||
# instances is not allowed in this mode.
|
# instances is not allowed in this mode.
|
||||||
|
|
|
@ -20,6 +20,7 @@ type config struct {
|
||||||
TemplatesPath string
|
TemplatesPath string
|
||||||
CustomCSS string
|
CustomCSS string
|
||||||
PostFormats []model.PostFormat
|
PostFormats []model.PostFormat
|
||||||
|
LogFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *config) IsValid() bool {
|
func (c *config) IsValid() bool {
|
||||||
|
@ -96,7 +97,7 @@ func Parse(r io.Reader) (c *config, err error) {
|
||||||
}
|
}
|
||||||
c.PostFormats = formats
|
c.PostFormats = formats
|
||||||
case "log_file":
|
case "log_file":
|
||||||
// ignore
|
c.LogFile = val
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("invalid config key " + key)
|
return nil, errors.New("invalid config key " + key)
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -6,4 +6,4 @@ require (
|
||||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.11
|
go 1.13
|
||||||
|
|
18
main.go
18
main.go
|
@ -26,7 +26,6 @@ func errExit(err error) {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
configFile := flag.String("f", "", "config file")
|
configFile := flag.String("f", "", "config file")
|
||||||
verbose := flag.Bool("v", false, "verbose mode")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if len(*configFile) > 0 {
|
if len(*configFile) > 0 {
|
||||||
|
@ -53,12 +52,25 @@ func main() {
|
||||||
customCSS = "/static/" + customCSS
|
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,
|
s := service.NewService(config.ClientName, config.ClientScope,
|
||||||
config.ClientWebsite, customCSS, config.SingleInstance,
|
config.ClientWebsite, customCSS, config.SingleInstance,
|
||||||
config.PostFormats, renderer)
|
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)
|
err = http.ListenAndServe(config.ListenAddress, handler)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errExit(err)
|
errExit(err)
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
package mastodon
|
package mastodon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -46,7 +42,6 @@ type Account struct {
|
||||||
Moved *Account `json:"moved"`
|
Moved *Account `json:"moved"`
|
||||||
Fields []Field `json:"fields"`
|
Fields []Field `json:"fields"`
|
||||||
Bot bool `json:"bot"`
|
Bot bool `json:"bot"`
|
||||||
Source *AccountSource `json:"source"`
|
|
||||||
Pleroma *AccountPleroma `json:"pleroma"`
|
Pleroma *AccountPleroma `json:"pleroma"`
|
||||||
MastodonAccount bool
|
MastodonAccount bool
|
||||||
}
|
}
|
||||||
|
@ -185,12 +180,13 @@ type Profile struct {
|
||||||
Source *AccountSource
|
Source *AccountSource
|
||||||
|
|
||||||
// Set the base64 encoded character string of the image.
|
// Set the base64 encoded character string of the image.
|
||||||
Avatar *multipart.FileHeader
|
Avatar string
|
||||||
Header *multipart.FileHeader
|
Header string
|
||||||
|
|
||||||
//Other settings
|
//Other settings
|
||||||
Bot *bool
|
Bot *bool
|
||||||
Pleroma *ProfilePleroma
|
Pleroma *ProfilePleroma
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfilePleroma struct {
|
type ProfilePleroma struct {
|
||||||
|
@ -203,116 +199,61 @@ type ProfilePleroma struct {
|
||||||
HideFollows *bool
|
HideFollows *bool
|
||||||
HideFollowersCount *bool
|
HideFollowersCount *bool
|
||||||
HideFollowsCount *bool
|
HideFollowsCount *bool
|
||||||
Avatar *multipart.FileHeader
|
|
||||||
Header *multipart.FileHeader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountUpdate updates the information of the current user.
|
// AccountUpdate updates the information of the current user.
|
||||||
func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, error) {
|
func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, error) {
|
||||||
var buf bytes.Buffer
|
params := url.Values{}
|
||||||
mw := multipart.NewWriter(&buf)
|
|
||||||
if profile.DisplayName != nil {
|
if profile.DisplayName != nil {
|
||||||
err := mw.WriteField("display_name", *profile.DisplayName)
|
params.Set("display_name", *profile.DisplayName)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if profile.Note != nil {
|
if profile.Note != nil {
|
||||||
err := mw.WriteField("note", *profile.Note)
|
params.Set("note", *profile.Note)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if profile.Locked != nil {
|
if profile.Locked != nil {
|
||||||
err := mw.WriteField("locked", strconv.FormatBool(*profile.Locked))
|
params.Set("locked", strconv.FormatBool(*profile.Locked))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if profile.Fields != nil {
|
if profile.Fields != nil {
|
||||||
for idx, field := range *profile.Fields {
|
for idx, field := range *profile.Fields {
|
||||||
err := mw.WriteField(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name)
|
params.Set(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name)
|
||||||
if err != nil {
|
params.Set(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = mw.WriteField(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if profile.Source != nil {
|
||||||
|
if profile.Source.Privacy != nil {
|
||||||
|
params.Set("source[privacy]", *profile.Source.Privacy)
|
||||||
}
|
}
|
||||||
if profile.Avatar != nil {
|
if profile.Source.Sensitive != nil {
|
||||||
f, err := profile.Avatar.Open()
|
params.Set("source[sensitive]", strconv.FormatBool(*profile.Source.Sensitive))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
fname := filepath.Base(profile.Avatar.Filename)
|
if profile.Source.Language != nil {
|
||||||
part, err := mw.CreateFormFile("avatar", fname)
|
params.Set("source[language]", *profile.Source.Language)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = io.Copy(part, f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if profile.Header != nil {
|
if profile.Avatar != "" {
|
||||||
f, err := profile.Header.Open()
|
params.Set("avatar", profile.Avatar)
|
||||||
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))
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := mw.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()}
|
|
||||||
var account Account
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &account, nil
|
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.
|
// GetAccountStatuses return statuses by specified accuont.
|
||||||
func (c *Client) GetAccountStatuses(ctx context.Context, id string, onlyMedia bool, onlyPinned bool, pg *Pagination) ([]*Status, error) {
|
func (c *Client) GetAccountStatuses(ctx context.Context, id string, onlyMedia bool, onlyPinned bool, pg *Pagination) ([]*Status, error) {
|
||||||
var statuses []*Status
|
var statuses []*Status
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
|
|
||||||
// AppConfig is a setting for registering applications.
|
// AppConfig is a setting for registering applications.
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
|
http.Client
|
||||||
Server string
|
Server string
|
||||||
ClientName string
|
ClientName string
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error
|
||||||
}
|
}
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
resp, err := httpClient.Do(req)
|
resp, err := appConfig.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -2,15 +2,19 @@
|
||||||
package mastodon
|
package mastodon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tomnomnom/linkheader"
|
"github.com/tomnomnom/linkheader"
|
||||||
|
@ -30,11 +34,6 @@ type Client struct {
|
||||||
config *Config
|
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 {
|
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)
|
u, err := url.Parse(c.config.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -58,12 +57,83 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if mr, ok := params.(*multipartRequest); ok {
|
} else if file, ok := params.(string); ok {
|
||||||
req, err = http.NewRequest(method, u.String(), mr.Data)
|
f, err := os.Open(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
} else {
|
||||||
if method == http.MethodGet && pg != nil {
|
if method == http.MethodGet && pg != nil {
|
||||||
u.RawQuery = pg.toValues().Encode()
|
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.
|
// NewClient return new mastodon API client.
|
||||||
func NewClient(config *Config) *Client {
|
func NewClient(config *Config) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
Client: httpClient,
|
Client: http.DefaultClient,
|
||||||
config: config,
|
config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,16 +219,6 @@ func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI st
|
||||||
return c.authenticate(ctx, params)
|
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 {
|
func (c *Client) authenticate(ctx context.Context, params url.Values) error {
|
||||||
u, err := url.Parse(c.config.Server)
|
u, err := url.Parse(c.config.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
package mastodon
|
package mastodon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
"encoding/json"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatusPleroma struct {
|
type StatusPleroma struct {
|
||||||
|
@ -229,51 +224,6 @@ func (c *Client) GetTimelineHome(ctx context.Context, pg *Pagination) ([]*Status
|
||||||
return statuses, nil
|
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.
|
// GetTimelinePublic return statuses from public timeline.
|
||||||
func (c *Client) GetTimelinePublic(ctx context.Context, isLocal bool, instance string, pg *Pagination) ([]*Status, error) {
|
func (c *Client) GetTimelinePublic(ctx context.Context, isLocal bool, instance string, pg *Pagination) ([]*Status, error) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
|
@ -407,35 +357,30 @@ func (c *Client) Search(ctx context.Context, q string, qType string, limit int,
|
||||||
return &results, nil
|
return &results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) {
|
// UploadMedia upload a media attachment from a file.
|
||||||
f, err := fh.Open()
|
func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) {
|
||||||
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()}
|
|
||||||
var attachment Attachment
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,11 +177,6 @@ type FiltersData struct {
|
||||||
Filters []*mastodon.Filter
|
Filters []*mastodon.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfileData struct {
|
|
||||||
*CommonData
|
|
||||||
User *mastodon.Account
|
|
||||||
}
|
|
||||||
|
|
||||||
type MuteData struct {
|
type MuteData struct {
|
||||||
*CommonData
|
*CommonData
|
||||||
User *mastodon.Account
|
User *mastodon.Account
|
||||||
|
|
|
@ -36,7 +36,6 @@ const (
|
||||||
SettingsPage = "settings.tmpl"
|
SettingsPage = "settings.tmpl"
|
||||||
UserEditPage = "useredit.tmpl"
|
UserEditPage = "useredit.tmpl"
|
||||||
FiltersPage = "filters.tmpl"
|
FiltersPage = "filters.tmpl"
|
||||||
ProfilePage = "profile.tmpl"
|
|
||||||
MutePage = "mute.tmpl"
|
MutePage = "mute.tmpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -69,7 +68,7 @@ func (c *client) redirect(url string) {
|
||||||
c.w.WriteHeader(http.StatusFound)
|
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")
|
csrf := c.r.FormValue("csrf_token")
|
||||||
ref := c.r.URL.RequestURI()
|
ref := c.r.URL.RequestURI()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -101,9 +100,6 @@ func (c *client) authenticate(t int, instance string) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.s = sess
|
c.s = sess
|
||||||
if len(instance) > 0 && c.s.Instance != instance {
|
|
||||||
return errors.New("invalid instance")
|
|
||||||
}
|
|
||||||
c.Client = mastodon.NewClient(&mastodon.Config{
|
c.Client = mastodon.NewClient(&mastodon.Config{
|
||||||
Server: "https://" + c.s.Instance,
|
Server: "https://" + c.s.Instance,
|
||||||
ClientID: c.s.ClientID,
|
ClientID: c.s.ClientID,
|
||||||
|
|
|
@ -149,20 +149,12 @@ func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
|
||||||
title = "Local Timeline"
|
title = "Local Timeline"
|
||||||
case "remote":
|
case "remote":
|
||||||
if len(instance) > 0 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
title = "Remote Timeline"
|
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":
|
case "twkn":
|
||||||
statuses, err = c.GetTimelinePublic(c.ctx, false, "", &pg)
|
statuses, err = c.GetTimelinePublic(c.ctx, false, "", &pg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -870,55 +862,6 @@ func (svc *service) FiltersPage(c *client) (err error) {
|
||||||
return svc.renderer.Render(c.rctx, c.w, renderer.FiltersPage, data)
|
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) {
|
func (s *service) SingleInstance() (instance string, ok bool) {
|
||||||
if len(s.instance) > 0 {
|
if len(s.instance) > 0 {
|
||||||
instance = s.instance
|
instance = s.instance
|
||||||
|
@ -1063,9 +1006,6 @@ func (s *service) NewSessionRegister(c *client, instance string, reason string,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Signout(c *client) (err error) {
|
|
||||||
return c.RevokeToken(c.ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) Post(c *client, content string, replyToID string,
|
func (s *service) Post(c *client, content string, replyToID string,
|
||||||
format string, visibility string, isNSFW bool, spoilerText string,
|
format string, visibility string, isNSFW bool, spoilerText string,
|
||||||
|
|
|
@ -2,9 +2,7 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
@ -26,7 +24,7 @@ const (
|
||||||
CSRF
|
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()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
writeError := func(c *client, err error, t int, retry bool) {
|
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,
|
r: req,
|
||||||
}
|
}
|
||||||
|
|
||||||
if verbose {
|
|
||||||
defer func(begin time.Time) {
|
defer func(begin time.Time) {
|
||||||
log.Printf("path=%s, err=%v, took=%v\n",
|
logger.Printf("path=%s, err=%v, took=%v\n",
|
||||||
req.URL.Path, err, time.Since(begin))
|
req.URL.Path, err, time.Since(begin))
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
|
||||||
|
|
||||||
var ct string
|
var ct string
|
||||||
switch rt {
|
switch rt {
|
||||||
|
@ -67,7 +63,7 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
}
|
}
|
||||||
c.w.Header().Add("Content-Type", ct)
|
c.w.Header().Add("Content-Type", ct)
|
||||||
|
|
||||||
err = c.authenticate(at, s.instance)
|
err = c.authenticate(at)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err, rt, req.Method == http.MethodGet)
|
writeError(c, err, rt, req.Method == http.MethodGet)
|
||||||
return
|
return
|
||||||
|
@ -82,7 +78,7 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
rootPage := handle(func(c *client) error {
|
rootPage := handle(func(c *client) error {
|
||||||
err := c.authenticate(SESSION, "")
|
err := c.authenticate(SESSION)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errInvalidSession {
|
if err == errInvalidSession {
|
||||||
c.redirect("/signin")
|
c.redirect("/signin")
|
||||||
|
@ -207,6 +203,43 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
return s.SearchPage(c, sq, qType, offset)
|
return s.SearchPage(c, sq, qType, offset)
|
||||||
}, SESSION, HTML)
|
}, 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 {
|
settingsPage := handle(func(c *client) error {
|
||||||
return s.SettingsPage(c)
|
return s.SettingsPage(c)
|
||||||
}, SESSION, HTML)
|
}, SESSION, HTML)
|
||||||
|
@ -215,57 +248,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
return s.FiltersPage(c)
|
return s.FiltersPage(c)
|
||||||
}, SESSION, HTML)
|
}, 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 {
|
signin := handle(func(c *client) error {
|
||||||
instance := c.r.FormValue("instance")
|
instance := c.r.FormValue("instance")
|
||||||
url, sess, err := s.NewSession(c, instance)
|
url, sess, err := s.NewSession(c, instance)
|
||||||
|
@ -768,10 +750,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
}, CSRF, HTML)
|
}, CSRF, HTML)
|
||||||
|
|
||||||
signout := handle(func(c *client) error {
|
signout := handle(func(c *client) error {
|
||||||
err := s.Signout(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.unsetSession()
|
c.unsetSession()
|
||||||
c.redirect("/")
|
c.redirect("/")
|
||||||
return nil
|
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("/aboutinstance", aboutInstance).Methods(http.MethodGet)
|
||||||
r.HandleFunc("/emojis", emojisPage).Methods(http.MethodGet)
|
r.HandleFunc("/emojis", emojisPage).Methods(http.MethodGet)
|
||||||
r.HandleFunc("/search", searchPage).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("/settings", settingsPage).Methods(http.MethodGet)
|
||||||
r.HandleFunc("/filters", filtersPage).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("/signin", signin).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/signup", signup).Methods(http.MethodPost)
|
r.HandleFunc("/signup", signup).Methods(http.MethodPost)
|
||||||
r.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet)
|
r.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet)
|
||||||
|
|
|
@ -286,12 +286,6 @@ function onPaste(e) {
|
||||||
fp.files = dt.files;
|
fp.files = dt.files;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeydown(e) {
|
|
||||||
if (e.key == 'Enter' && e.ctrlKey) {
|
|
||||||
document.querySelector(".post-form").submit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
checkCSRFToken();
|
checkCSRFToken();
|
||||||
checkAntiDopamineMode();
|
checkAntiDopamineMode();
|
||||||
|
@ -332,10 +326,8 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var pf = document.querySelector(".post-form")
|
var pf = document.querySelector(".post-form")
|
||||||
if (pf) {
|
if (pf)
|
||||||
pf.addEventListener("paste", onPaste);
|
pf.addEventListener("paste", onPaste);
|
||||||
pf.addEventListener("keydown", onKeydown);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// @license-end
|
// @license-end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
frame, body {
|
body {
|
||||||
background-color: #d2d2d2;
|
background-color: #d2d2d2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,14 +167,15 @@ textarea {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
font-family: initial;
|
font-family: initial;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-content {
|
.post-content {
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#css, #bio {
|
#css {
|
||||||
|
box-sizing: border-box;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,14 +442,9 @@ img.emoji {
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-edit-link {
|
|
||||||
font-size: 8pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-list-item {
|
.user-list-item {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin: 0 0 4px 0;
|
margin: 0 0 12px 0;
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -600,41 +596,6 @@ kbd {
|
||||||
color: #789922;
|
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 {
|
.dark {
|
||||||
background-color: #222222;
|
background-color: #222222;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
|
@ -645,7 +606,7 @@ kbd {
|
||||||
color: #81a2be;
|
color: #81a2be;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .post-content {
|
.dark textarea {
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
border: 1px solid #444444;
|
border: 1px solid #444444;
|
||||||
color: #eaeaea;
|
color: #eaeaea;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{with .Data}}
|
{{with .Data}}
|
||||||
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
|
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
|
||||||
<div class="page-title"> Mute {{EmojiFilter (HTML .User.DisplayName) .User.Emojis | Raw}} @{{.User.Acct}} </div>
|
<div class="page-title"> Mute {{.User.Acct}} </div>
|
||||||
|
|
||||||
<form action="/mute/{{.User.ID}}" method="POST">
|
<form action="/mute/{{.User.ID}}" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
|
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
|
||||||
|
|
|
@ -12,9 +12,6 @@
|
||||||
<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>
|
||||||
<a class="profile-edit-link" href="/profile" title="edit profile" target="_top">
|
|
||||||
edit
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info-details-nav">
|
<div class="user-info-details-nav">
|
||||||
<a class="nav-link" href="/timeline/home" accesskey="1" title="Home timeline (1)">home</a>
|
<a class="nav-link" href="/timeline/home" accesskey="1" title="Home timeline (1)">home</a>
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
{{with .Data}}
|
|
||||||
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
|
|
||||||
<div class="page-title"> Edit Profile </div>
|
|
||||||
|
|
||||||
<form class="profile-form" action="/profile" method="POST" enctype="multipart/form-data">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
|
|
||||||
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
|
|
||||||
<div class="profile-form-field">
|
|
||||||
<div class="block-label">
|
|
||||||
<label for="avatar">Avatar</label> -
|
|
||||||
<input class="btn-link" type="submit" formaction="/profile/delavatar" formmethod="POST" value="delete">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="{{.User.Avatar}}" target="_blank">
|
|
||||||
<img class="profile-avatar" src="{{.User.Avatar}}" alt="profile-avatar" height="96">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div><input id="avatar" name="avatar" type="file"></div>
|
|
||||||
</div>
|
|
||||||
<div class="profile-form-field">
|
|
||||||
<div class="block-label">
|
|
||||||
<label for="banner">Banner</label> -
|
|
||||||
<input class="btn-link" type="submit" formaction="/profile/delbanner" formmethod="POST" value="delete">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="{{.User.Header}}" target="_blank">
|
|
||||||
<img class="profile-banner" src="{{.User.Header}}" alt="profile-banner" height="120">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<input id="banner" name="banner" type="file">
|
|
||||||
</div>
|
|
||||||
<div class="profile-form-field">
|
|
||||||
<div class="block-label"><label for="name">Name</label></div>
|
|
||||||
<div><input id="name" name="name" type="text" value="{{.User.DisplayName}}"></div>
|
|
||||||
</div>
|
|
||||||
<div class="profile-form-field">
|
|
||||||
<div class="block-label"><label for="bio">Bio</label></div>
|
|
||||||
<textarea id="bio" name="bio" cols="80" rows="8">{{.User.Source.Note}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="profile-form-field">
|
|
||||||
<div class="block-label"><label>Metadata</label></div>
|
|
||||||
{{range $i, $f := .User.Source.Fields}}
|
|
||||||
<div class="profile-field">
|
|
||||||
<input id="field-name-{{$i}}" name="field-name-{{$i}}" type="text" value="{{$f.Name}}" placeholder="name">
|
|
||||||
<input id="field-value-{{$i}}" name="field-value-{{$i}}" type="text" value="{{$f.Value}}" placeholder="value">
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="profile-form-field">
|
|
||||||
<input id="locked" name="locked" type="checkbox" value="true" {{if .User.Locked}}checked{{end}}>
|
|
||||||
<label for="locked">Require manual approval of follow requests</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit"> Save </button>
|
|
||||||
<button type="reset"> Reset </button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{template "footer.tmpl"}}
|
|
||||||
{{end}}
|
|
|
@ -11,7 +11,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="status-dname"> {{EmojiFilter (HTML .DisplayName) .Emojis | Raw}} </div>
|
<div class="status-dname"> {{EmojiFilter (HTML .DisplayName) .Emojis | Raw}} </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>
|
||||||
</div>
|
</div>
|
||||||
<form class="d-inline" action="/accept/{{.ID}}" method="post" target="_self">
|
<form class="d-inline" action="/accept/{{.ID}}" method="post" target="_self">
|
||||||
|
|
|
@ -4,15 +4,14 @@
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
</head>
|
</head>
|
||||||
<frameset cols="424px,*">
|
<frameset cols="424px,*">
|
||||||
<frameset rows="316px,*">
|
<frameset rows="316px,*">
|
||||||
<frame name="nav" src="/nav" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
|
<frame name="nav" src="/nav">
|
||||||
<frame name="notification" src="/notifications" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
|
<frame name="notification" src="/notifications">
|
||||||
</frameset>
|
</frameset>
|
||||||
<frame name="main" src="/timeline/home" {{if $.Ctx.DarkMode}}class="dark"{{end}}>
|
<frame name="main" src="/timeline/home">
|
||||||
</frameset>
|
</frameset>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -99,8 +99,8 @@
|
||||||
{{if (or .Content .SpoilerText)}}
|
{{if (or .Content .SpoilerText)}}
|
||||||
<div class="status-content">
|
<div class="status-content">
|
||||||
{{if .Sensitive}}[NSFW]<br/>{{end}}
|
{{if .Sensitive}}[NSFW]<br/>{{end}}
|
||||||
{{- if .SpoilerText}}{{EmojiFilter (HTML .SpoilerText) .Emojis | Raw}}<br/>{{end -}}
|
{{if .SpoilerText}}[{{EmojiFilter (HTML .SpoilerText) .Emojis | Raw}}]<br/>{{end}}
|
||||||
{{- StatusContentFilter .Content .Emojis .Mentions | Raw -}}
|
{{StatusContentFilter .Content .Emojis .Mentions | Raw}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{$st_id := .ID}}
|
{{$st_id := .ID}}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<div class="page-title"> User </div>
|
<div class="page-title"> User </div>
|
||||||
|
|
||||||
<div class="user-info-container">
|
<div class="user-info-container">
|
||||||
|
<div>
|
||||||
<div class="user-profile-img-container">
|
<div class="user-profile-img-container">
|
||||||
<a class="img-link" href="{{.User.Avatar}}" target="_blank">
|
<a class="img-link" href="{{.User.Avatar}}" target="_blank">
|
||||||
<img class="user-profile-img" src="{{.User.Avatar}}" alt="profile-avatar" height="96" />
|
<img class="user-profile-img" src="{{.User.Avatar}}" alt="profile-avatar" height="96" />
|
||||||
|
@ -127,17 +128,18 @@
|
||||||
<summary>About user</summary>
|
<summary>About user</summary>
|
||||||
<div class="user-profile-decription">
|
<div class="user-profile-decription">
|
||||||
[User created: {{.User.CreatedAt}}]<br>
|
[User created: {{.User.CreatedAt}}]<br>
|
||||||
{{- EmojiFilter .User.Note .User.Emojis | Raw -}}
|
{{EmojiFilter .User.Note .User.Emojis | Raw}}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{{if .User.Fields}}
|
{{if .User.Fields}}
|
||||||
<div class="user-fields">
|
<div class="user-fields">
|
||||||
{{range .User.Fields}}
|
{{range .User.Fields}}
|
||||||
<div>{{- EmojiFilter (HTML .Name) $.Data.User.Emojis | Raw}} - {{EmojiFilter .Value $.Data.User.Emojis | Raw -}}</div>
|
<div>{{EmojiFilter .Name $.Data.User.Emojis | Raw}} - {{EmojiFilter .Value $.Data.User.Emojis | Raw}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{if eq .Type ""}}
|
{{if eq .Type ""}}
|
||||||
<div class="page-title"> Statuses </div>
|
<div class="page-title"> Statuses </div>
|
||||||
|
|
Loading…
Reference in a new issue